Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1803249
  • 博文数量: 274
  • 博客积分: 2366
  • 博客等级: 大尉
  • 技术积分: 1880
  • 用 户 组: 普通用户
  • 注册时间: 2007-04-22 09:37
文章分类

全部博文(274)

文章存档

2022年(1)

2020年(10)

2019年(7)

2018年(18)

2017年(26)

2016年(32)

2015年(43)

2014年(30)

2013年(44)

2012年(36)

2011年(17)

2010年(10)

分类: LINUX

2020-08-06 14:02:26

这一篇将会聊聊C++中一个极具迷惑性的关键字 ———— inline。

虽然只是一个小小的关键字,但要是没有真正了解它,也是很容易踩坑的。
本文暂时不讨论 inline variable,主要讨论 inline function。

一、什么是 inline
或许你对它有那么一点点熟悉,但是又说不清。它的中文翻译为“内联”。它经常跟一个东西共同出现,称为“内联函数(inline function)”。正是这样的翻译,对新手产生了太多的误会。那么什么是 inline 呢?现在暂时不做解答。接下来我们会经常翻 cppreference,所以你可以先点开放到一旁,然后我们进入正题。

二、inline 之初印象
上网搜一下,C++ inline 的作用,你看到的都是什么?都是说 inline 函数可以自动把函数展开呀,可以减小函数调用的开销呀……还会好心提醒你,太大的函数不宜用 inline 呀!会导致代码膨胀呀!只有那些短小精悍的、经常调用的函数使用 inline 才能看到非常明显的效果呀…………我在 N 年前,也是这么傻傻的,所以看到短函数就想加 inline,唯恐性能变差。于是,在印象里,inline 就跟优化挂上钩了。

三、inline 之初体验
我们先从简单的例子开始,新建一个 main.cpp 源文件:

#include
int max(int a, int b)
{
return a < b ? b : a;
}
int main()
{
int res = max(1, 2);
std::cout << res << “\n”;
}
我们编译运行,好,通过了。看到这个函数手痒了是吧,好我们给它加上 inline,再次编译运行,也通过了。到这里还没有问题,具体它的优化我们暂时不去分析。我们要养成一个工程习惯嘛!这是个简单的例子,但实际中我们写的代码可能很多,可能有很多很多个类似于 max 这样的函数,于是我们就想着把他们区分开,于是我们又创建了一个 a.h 头文件,把 max 扔进去,我们还想着接口与实现分离,于是我们创建一个 a.cpp 源文件来实现函数的定义,文件结构变成了这样:

// a.h
int max(int a, int b);
// a.cpp
#include “a.h”
int max(int a, int b)
{
return a < b ? b : a;
}
// main.cpp
#include
#include “a.h”
int main()
{
int res = max(1, 2);
std::cout << res << “\n”;
}
我们运行一遍,没有问题,然后我们试着给 max 函数加上 inline,你知道,Ⅰ.inline 要跟函数的定义放在一块,所以我们就要在 a.cpp 给它加上:

inline int max(int a, int b){…}
我们再次编译运行,WTF!!怎么报错了!!不就加了个 inline 吗?!! 冷静了一会,我们看看报错信息,如果你用的是 VS,那么你大概会看到这样的提示:

1>main.obj : error LNK2019: 无法解析的外部符号 “int __cdecl max(int,int)” (?max@@YAHHH@Z),该符号在函数 _main 中被引用
1>C:\Users\Alinshans\documents\visual studio 2017\Projects\test\Debug\test.exe : fatal error LNK1120: 1 个无法解析的外部命令
是不是感觉有点熟悉?似曾相识?如果你用的是 g++ 去编译,那么大概会得到这样的提示:

main.cpp:(.text+0x13): undefined reference to `max(int, int)’
要是你对C/C++编译的过程有一点点了解的话,我们继续尝试把编译过程和链接过程分开来做:

$ g++ -c main.cpp
$ g++ main.o -o a.out
你就会发现,执行第一条命令(编译)时,是没有错的,执行第二条命令(链接)时,就报错了。

main.o:main.cpp:(.text+0x18): undefined reference to `max(int, int)’
collect2.exe: error: ld returned 1 exit status
这里简单说明一下,大多数的建置环境都是在编译过程进行 inlining(为了替换函数调用,编译器需要知道函数的实体长什么样,这就解释了 Ⅰ),某些可以在连接期完成,少数的可以在运行期完成。我们只考虑绝大部分情况:inlining 在大多数 C++ 程序中是编译期行为。
好了,我们讲回来,为什么会出现这个链接错误呢?注意到刚刚打开的网页 这里的第二、三条 :

  1. The definition of an inline function or variable (since C++17) must be present in the translation unit where it is accessed (not necessarily before the point of access).
  2. An inline function or variable (since C++17) with external linkage (e.g. not declared static) has the following additional properties:
  3. It must be declared inline in every translation unit.
  4. It has the same address in every translation unit.

这里提到了 external linkage,若想详细了解可以看 这里。嫌太长不看的你只需要知道我们定义的 max 函数,具有 external linkage,那么它就要满足:

在你需要引用它的编译单元可见
在每个编译单元都要声明为 inline
用人话讲就是,inline function 的定义需要出现在每个编译单元(.cpp/.cc/.cxx 等),也就是说你要把 main.cpp 写成这样:

// main.cpp
#include
#include “a.h”
inline int max(int a, int b)
{
return a < b ? b : a;
}
int main()
{
int res = max(1, 2);
std::cout << res << “\n”;
}
好了,这下就没问题了,可以编译通过了。但是,这就需要我们把每一个在源文件内定义的 inline function,复制到每一个需要引用它的源文件。这样的事情我想没有谁愿意做。所以,一般情况下,Ⅱ. 在源文件中定义的函数,不要声明为 inline,除非你有特殊用途。

四、inline 之再体验
我们刚刚已经尝试了在源文件中给函数加上或不加上 inline 的区别,接下来就试一试在头文件中给函数加上或不加上 inline 的区别吧!我们在 a.h 中新增一个函数:

// a.h
int max(int a, int b);
int min(int a, int b)
{
return a < b ? a : b;
}
好,a.cpp 文件依然没有变:

// a.cpp
#include “a.h”
inline int max(int a, int b)
{
return a < b ? b : a;
}
在 main.cpp 中引用这个 min 函数:

// main.cpp
#include
#include “a.h”
inline int max(int a, int b)
{
return a < b ? b : a;
}
int main()
{
int res = max(1, 2);
int res2 = min(1, 2);
std::cout << res << " " << res2 << “\n”;
}
好我们编译运行一下,WTF!!怎么又报错了!??这次又是什么鬼!?我们仔细看看,在 VS 下的提示是:

1>main.obj : error LNK2005: “int __cdecl min(int,int)” (?min@@YAHHH@Z) 已经在 a.obj 中定义
1>C:\Users\Alinshans\documents\visual studio 2017\Projects\test\Debug\test.exe : fatal error LNK1169: 找到一个或多个多重定义的符号
是不是感觉经常看到这类的错误摸不着头脑?(注意,若使用 g++ 编译运行时,没有报错,并且运行结果对了,这不是值得侥幸的,它实际上有问题)为什么会提示重定义呢?我们稍微思考一下就能想明白:在 a.h 中定义了 min 这个函数,而 a.h 同时被 a.cpp 和 main.cpp include 了,而 include 的作用其实就相当于复制黏贴一遍,所以在编译 a.cpp 和 main.cpp 时,会产生两个相同的符号,也就是我们所看到的错误提示。

五、什么时候应该使用 inline
我们再联系一下之前在源文件中添加 inline 得出的结论,你想到了什么?inline function 的定义要在每个编译单元可见,对不对?所以我们尝试一下把 min 函数声明为 inline:

// a.h
int max(int a, int b);
inline int min(int a, int b)
{
return a < b ? a : b;
}
再次编译运行,没有报错!而且结果也是正确的!那为什么我们使用 inline 时,就不会有这个错误呢?

我们还是看到刚刚的链接,看到 第一条:

  1. There may be more than one definition of an inline function or variable (since C++17) in the program as long as each definition appears in a different translation unit and (for non-static inline functions and variables (since C++17)) all definitions are identical. For example, an inline function or an inline variable (since C++17) may be defined in a header file that is #include’d in multiple source files.

用人话说就是,inline function 是具有这样的属性滴,什么属性捏,就是可以出现在多个编译单元,并且它们的定义都是相同滴。
所以我们在头文件内定义的函数(函数模板另说),就必须要加上 inline 声明,这样做就是告诉编译器,我们是 inline function,虽然定义出现了很多次,但是都是相同滴,老哥你别报错!
所以,Ⅲ. 当函数定义出现在头文件时,使用 inline 。

六、inline 与类成员函数、模板
以上我们讨论了四种情况,接下来这个部分应该也是很让新手纠结的。因为一个类,可能会有很多 getter/setter 之类的短小的函数,于是就会去纠结要不要加 inline。那么我们同样分类来讨论,根据类的成员函数定义的位置,有以下三种(假设类的声明在 a.h,定义在 a.cpp):

在头文件中,在类内定义
在头文件中,在类外定义
在源文件中定义
我们一个一个来谈谈。首先是在类内定义的,需不需要加 inline 呢?还是看到我们打开的页面,最上面这里:

A function defined entirely inside a class/struct/union definition, whether it’s a member function or a non-member friend function, is implicitly an inline function.

在类内定义的函数,是隐式 inline 的,所以不需要你加 inline,而且,LLVM CodingStandards 也是这样提出的:

Don’t use inline when defining a function in a class definition

所以,在类内定义的函数,是不需要加 inline 滴,当然加了也不会错啦~

我们再讲讲在源文件中定义的,要是你认真看了之前的内容,你就应该想到,不要把 inline function 的定义放在源文件中。所以如果你的类成员函数定义在了源文件中,也是不可以加 inline。
还剩下一个,其实我想不到什么理由,可以让成员函数既不在类内定义,也不在源文件中定义,偏偏要在头文件中并且在类外定义(模板类成员函数/类模板成员函数除外)。如果成员函数比较短小,那么你就可以直接定义在类内,否则就定义在源文件中。所以剩下这种情况的写法,我是不推荐的,如果非得要定义在头文件且在类外,那就必须要声明为 inline,否则也会有重定义的错误。
所以,简单起见,Ⅳ. 类成员函数都不要加 inline。

然后再讲模板,包括了函数模板、类模板成员函数和模板类成员函数。有点晕是吧,反正就是跟模板扯上关系了的,这些函数都自带 inline 语义。也就是说,你把刚刚的那个 a.h 文件里的 min 函数改成模板:

template
T min(T a, T b)
{
return a < b ? a : b;
}
加不加 inline,都是可以正常运行的。所以,Ⅴ. 模板相关函数不需要声明为 inline,也具有 inline 的语义。

七、inline 与优化
刚刚说了那么多 inline 的用法,好像跟你了解的 优化 没扯上什么关系啊!那么现在就到了要摧毁印象的时候了。我们就先用这段原本的代码来测试:

// main.cpp
#include
#include
inline int max(int a, int b)
{
return a < b ? b : a;
}
int main()
{
int res = max(1, 2);
std::printf("%d\n", res);
}
现在,max 函数是声明为 inline 的,我们可以看反汇编代码,来看看 max 是否有调用。如果使用 g++,可以分别运行以下三条命令:

$ g++ -E main.cpp -o main.i
$ g++ -S main.i -o main.s
$ g++ -O2 -S main.i -o main2.s
然后 main.s 和 main2.s 就是分别未使用优化和使用了 O2 优化后的反汇编代码。
在 VS 下看反汇编就非常简单了,随便设置一个断点,然后点调试->开始调试,等调试开始后,点调试->窗口->反汇编,就可以看到反汇编代码了。因为 VS 的反汇编的代码比较清晰好看,所以就以 VS 中的反汇编为例。
我们先在 Debug 模式下,查看反汇编代码(主要部分):

int res = max(1, 2);
002218AE push 2
002218B0 push 1
002218B2 call max (02212BCh)
002218B7 add esp,8
002218BA mov dword ptr [res],eax
std::printf("%d\n", res);
002218BD mov eax,dword ptr [res]
002218C0 push eax
002218C1 push offset string “%d\n” (0227B30h)
002218C6 call _printf (022132Fh)
002218CB add esp,8
我们可以看到,是有调用 max 函数的。我们再切换到 Release 模式,查看反汇编代码:

int res = max(1, 2);
std::printf("%d\n", res);
00131040 push 2
00131042 push offset string “%d\n” (01320F8h)
int res = max(1, 2);
std::printf("%d\n", res);
00131047 call printf (0131010h)
0013104C add esp,8
是的,max 函数的调用已经不见了,不过你认为这是拜你加上的 inline 所赐的吗?我们去掉 inline ,再重复一遍刚刚的过程,你会发现,结果是一模一样的。
你没有死心,说,这个函数太简单了,我是编译器我也能看得出来怎么优化,要是函数复杂一点,比如有循环、递归什么的,编译器就不会自动优化了!
那好吧,我们把 main.cpp 改成这样:

#include
#include
int test(int i)
{
int x = 0;
for (int j = 0; j < i; ++j)
{
x += j;
}
return x;
}
int main()
{
std::printf("%d\n", test(100));
}
在 Debug 下反汇编:

std::printf("%d\n", test(100));
010118AE push 64h
010118B0 call test (0101136Bh)
010118B5 add esp,4
010118B8 push eax
010118B9 push offset string “%d\n” (01017B30h)
010118BE call _printf (0101132Fh)
010118C3 add esp,8
在 Release 下反汇编:

std::printf("%d\n", test(100));
00F31042 xor ecx,ecx
00F31044 xor eax,eax
std::printf("%d\n", test(100));
00F31046 xor edx,edx
00F31048 xor esi,esi
00F3104A xor edi,edi
00F3104C nop dword ptr [eax]
00F31050 inc edi
00F31051 add esi,2
00F31054 add edx,3
00F31057 add ecx,eax
00F31059 add edi,eax
00F3105B add esi,eax
00F3105D add edx,eax
00F3105F add eax,4
00F31062 cmp eax,64h
00F31065 jl main+10h (0F31050h)
00F31067 lea eax,[edx+esi]
00F3106A add eax,edi
00F3106C add ecx,eax
00F3106E push ecx
00F3106F push offset string “%d\n” (0F320F8h)
00F31074 call printf (0F31010h)
00F31079 add esp,8
喔,不要看它这么长,其实它是直接算出结果的了,所以已经没有 test 的调用了。这次看用 g++ 生成的反汇编会更清晰一些:
不开优化:

main:
.LFB1022:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl $100, %edi
call _Z4testi
movl %eax, %esi
movl $.LC0, %edi
movl $0, %eax
call printf
movl $0, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
开 O2 优化:

main:
.LFB1022:
.cfi_startproc
subq $8, %rsp
.cfi_def_cfa_offset 16
movl $4950, %esi
movl $.LC1, %edi
xorl %eax, %eax
call printf
xorl %eax, %eax
addq $8, %rsp
.cfi_def_cfa_offset 8
ret
看到了吧,这一次的 test 函数,我没有加 inline,在开启编译器优化的情况下,它还是可以自动去优化的。
你还会说,那啥,那啥……你还想说什么,自己去验证吧。我可以做最后一个实验。
现在把 main.cpp 改成这样:

// main.cpp
#include
#include
#include
inline int test(int i)
{
int prime[100];
int k = 0;
for (int n = 2; n <= i; ++n)
{
bool is_prime = true;
for (int j = 2; j <= static_cast(std::sqrt(n)); ++j)
{
if (n % j == 0)
{
is_prime = false;
break;
}
}
if (is_prime)
{
prime[k] = n;
++k;
}
}
int sum = 0;
for (int n = 0; n < k; ++n)
{
sum += prime[n];
}
return sum;
}
int main()
{
std::printf("%d\n", test(100));
}
嗯…是有点儿长,我可是把 test 函数声明为 inline 的!然后在 Debug 下反汇编:

std::printf("%d\n", test(100));
00131ABE push 64h
00131AC0 call test (013102Dh)
00131AC5 add esp,4
00131AC8 push eax
00131AC9 push offset string “%d\n” (0137B30h)
00131ACE call _printf (0131339h)
00131AD3 add esp,8
在 Release 下反汇编:

std::printf("%d\n", test(100));
00FC1170 call test (0FC1040h)
00FC1175 push eax
00FC1176 push offset string “%d\n” (0FC20F8h)
00FC117B call printf (0FC1010h)
00FC1180 add esp,8
我已经声明了 inline,可是无论有没有开优化,它也不会去掉这个函数调用了。

八、inline 的真正意义
现在你该停下来思考思考了,什么是 inline ?是 “内联” 吗?inline 的意义是什么?发起一个 “内联” 请求吗?
但事实上,你会发现,有时候,你不用 inline,会报错;有时候,你用了 inline,又会报错。你期望使用 inline 可以优化程序效率,但貌似跟你加不加 inline 没有什么关系啊?
inline 的含义,似乎与 “优化”、“内联”,已经渐行渐远了。

好好的思考一会儿吧!

think.jpg

我们还是继续看打开的 cppreference,注意到这里有一段话:

The original intent of the inline keyword was to serve as an indicator to the optimizer that inline substitution of a function is preferred over function call, that is, instead of executing the function call CPU instruction to transfer control to the function body, a copy of the function body is executed without generating the call. This avoids overhead created by the function call (copying the arguments and retrieving the result) but it may result in a larger executable as the code for the function has to be repeated multiple times.
Since this meaning of the keyword inline is non-binding, compilers are free to use inline substitution for any function that’s not marked inline, and are free to generate function calls to any function marked inline. Those optimization choices do not change the rules regarding multiple definitions and shared statics listed above.

看不懂没关系,其实它就是说:在很久很久以前,inline 作为给编译器优化的提示符,它的含义是非绑定的,编译器可以自由的选择、决定是否对一个函数进行展开。而如今,编译器根本不需要这样的提示,如果它认为一个函数值得内联展开,它会自动展开,否则,即使你声明为 inline,它也会拒绝。
可以看看这一篇 SO上的回答:

It is said that inline hints to the compiler that you think the function should be inlined. That may have been true in 1998, but a decade later the compiler needs no such hints. Not to mention humans are usually wrong when it comes to optimizing code, so most compilers flat out ignore the ‘hint’.

static - the variable/function name cannot be used in other compilation units. Linker needs to make sure it doesn’t accidentally use a statically defined variable/function from another compilation unit.
extern - use this variable/function name in this compilation unit but don’t complain if it isn’t defined. The linker will sort it out and make sure all the code that tried to use some extern symbol has its address.
inline - this function will be defined in multiple compilation units, don’t worry about it. The linker needs to make sure all compilation units use a single instance of the variable/function.
现在你应该差不多能够理解了,现在的编译器,并不需要你用 inline 去提醒,不要小看搞编译器那帮人,除非你觉得自己的水平比他们的高,想着帮编译器优化的,一般人往往是错误的。

我们看到打开的 链接 里面还有一句框起来的话:

Because the meaning of the keyword inline for functions came to mean “multiple definitions are permitted” rather than “inlining is preferred”, that meaning was extended to variables.

翻译过来就是:Ⅵ. inline 的含义更多的是“允许多重定义”而不是“优先选择内联”。

醒悟了吗?inline 这个关键字,以及它的翻译,就是一个坑,它真正的含义并不是要去内联一个函数,而是表示 “老哥,别怕!无论你看到了多少个定义,但其实我们都是一样的!” 所以编译器看到一个 inline function,会允许它在不同的编译单元出现多次,因为它知道它们都是一样的,具有共同的内存地址。

最后希望看完这篇文章的童鞋们,都可以深刻的理解 C++ 的 inline。

九、总结
inline 要跟函数的定义放在一块
在源文件中定义的函数,不要声明为 inline
当函数在头文件中定义且有可能被多个源文件包含时,使用 inline
类成员函数不需要声明为 inline
模板相关的函数不需要声明为 inline,也具有 inline 的语义
inline 的含义更多的是“允许多重定义”而不是“优先选择内联”
inline 跟 “优化” 没有半毛钱的关系
※ 注:以上总结适用于不熟悉、不了解 inline 的同学。若对以上内容都清楚了解,使用 inline 的时候,清楚在做什么,会发生什么,那就随便怎么用啦!

阅读(1352) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~