内联的疑惑
写这篇文章的初衷源自于对netdata项目把C函数声明为static inline的用法不解。从语言特性上看,内联函数在编译时展开,本来就没有符号,再加个static不是多此一举吗?更有甚者,netdata对大部的C函数都声明为static inline(号称是为了追求极致的执行性能,个人存疑),难道没有优化开关强制编译器对每个函数都进行内联推导吗?
而后在整改老代码过程中,需要把复杂的仿函数宏重构成内联函数。其间发现很多同学对内联也不甚了解(me too:()。希望这篇文章可以拨开迷雾,帮助大家理清内联的细节。
在“知新”之前,我们先“温故”。先把时间坐标前移到上个世纪末。C99就在那个时候问世。在C99新增的特性中,有一个来自于C++。这就是本文的主角内联函数。C不是无脑的拿来主义,除了语言特性和限制不同之外,在实现细节上跟C++有较大不同。最大的差异在于内联函数地址的选择。
内联函数有一个很重要的特性,在所有源文件引用的内联函数地址是一样的。这点,C和C++都满足。但是实现方式却有所不同。
下面通过一个实验来展开两者差异的分析。
验证代码说明
如下所示,在头文件utils.h中定义了一个内联函数get_tu_name:
#define TU_NAME_LEN 32
typedef void (*TU_NAME)(char *tu);
inline void get_tu_name(char *tu)
{
if (!tu) return;
strcpy(tu, TU);
}
在main.c除了分别以内联的方式和非内联的方式调用get_tu_name,也引用其函数地址。如下所示:
#include "utils.h"
int main(void)
{
char tu[TU_NAME_LEN] = {0};
get_tu_name(tu);
printf("%s inline tu name=%s, func=%p\n", __FILE__, tu, get_tu_name);
TU_NAME tu_name = get_tu_name;
show_non_inline_in_utils(tu_name, __FILE__);
show_utils();
return 0;
}
在另外一个源文件utils.c也会做同样的事。show_inline_in_utils以内联方式调用get_tu_name,show_non_inline_in_utils以非内联方式调用,这两个函数都引用了函数地址:
#include "utils.h"
void get_tu_name(char *tu);
void show_inline_in_utils(void);
void show_utils(void)
{
show_inline_in_utils();
TU_NAME tu_name = get_tu_name;
show_non_inline_in_utils(tu_name, __FILE__);
}
void show_inline_in_utils(void)
{
char tu[TU_NAME_LEN] = {0};
get_tu_name(tu);
printf("%s inline:tu name=%s, func=%p\n", __FILE__, tu, get_tu_name);
}
void show_non_inline_in_utils(TU_NAME tu_name, const char *file)
{
char tu[TU_NAME_LEN] = {0};
tu_name(tu);
printf("%s non inline:tu name=%s, func=%p\n", file, tu, tu_name);
}
为了测试内联函数是从属于哪个源文件,get_tu_name引用的TU是一个编译宏,其在makefile的编译规则中指定,如下的-DTU=XXX:
%.o: %.c
$(CC) $(OPTIM) --std=$(STD) -DTU=\"$(patsubst %.c,[%],$<)\" -c $< -o $@
main.c对应的TU为[main],utils.c则为[utils]。
验证环境:
- 编译器版本是gcc6.2.0
- OS是64位的MacOS 10.13.5
C内联分析(c99)
我们先看看gcc以C99标准编译后的输出:
gcc -O2 --std=c99 -DTU="[main]" -c main.c -o main.o
gcc -O2 --std=c99 -DTU="[utils]" -c utils.c -o utils.o
gcc ./main.o ./utils.o -o test_inline
echo "run test:" && ./test_inline
run test:
main.c inline tu name=[main], func=0x10bc7fcb0
main.c non inline:tu name=[utils], func=0x10bc7fcb0
utils.c inline:tu name=[utils], func=0x10bc7fcb0
utils.c non inline:tu name=[utils], func=0x10bc7fcb0
无论是内联还是非内联,get_tu_name的地址都是一样的。内联和非内联的差异在于内联使用的是源文件各自的版本,而非内联使用的是utils.c的版本(TU为[utils])。
这里有三个问题:
- 内联函数会编译成独立的函数,以及会生成符号吗?
我们先看看两个obj的符号表有没有get_tu_name:
in ./main.o:
0000000000000000 *UND* _get_tu_name
in ./utils.o:
0000000000000000 g F __TEXT,__text _get_tu_name
其中,utils.o有get_tu_name的全局符号,因为它被选中为内联函数的非内联版本的提供者(见第3点)。而main.o则是未定义符号,main.o没有生成符号,反而依赖于外部的get_tu_name。从这里可以看出内联函数没有全局符号。
从第二点的反汇编来看,show_inline_in_utils在调用get_tu_name的地方已经像宏一样展开了,而main.o在调用点展开后,get_tu_name既没有符号,也没有独立够的函数代码段。
更有甚者,如果内联函数在源文件没有引用,目标文件没有内联函数的半点信息,就像代码里面就没有内联函数一样。如果没有内联函数,我们只有用仿函数宏或者静态函数来达到这个目的。utils.h还有另外一个没人调用的内联函数convert2,有兴趣可以编译看看:)
综上所述,内联函数一般情况下会在调用点展开,不会生成符号和独立代码段。而且没有调用的内联函数不占有目标文件的空间。特殊情况是指编译器驳回了内联请求,不将函数内联。
- 内联版本和非内联版本同时存在时,编译器会选哪个?
标准其实没有规定,留给编译器自己决定。所以在回答这个问题前,我们先看看utils.o的反汇编:
$ objdump -d utils.o
utils.o: file format Mach-O 64-bit x86-64
Disassembly of section __TEXT,__text:
_get_tu_name:
0: 48 85 ff testq %rdi, %rdi
3: 74 0d je 13 <_get_tu_name+0x12>
5: 48 b8 5b 75 74 69 6c 73 5d 00 movabsq $26304082296993115, %rax
f: 48 89 07 movq %rax, (%rdi)
12: c3 retq
13: 66 66 66 66 2e 0f 1f 84 00 00 00 00 00 nopw %cs:(%rax,%rax)
_show_inline_in_utils:
20: 48 b8 5b 75 74 69 6c 73 5d 00 movabsq $26304082296993115, %rax
2a: 48 83 ec 28 subq $40, %rsp
2e: 48 89 04 24 movq %rax, (%rsp)
32: 48 89 e2 movq %rsp, %rdx
35: 31 c0 xorl %eax, %eax
37: 48 8d 0d 00 00 00 00 leaq (%rip), %rcx
3e: 48 c7 44 24 08 00 00 00 00 movq $0, 8(%rsp)
47: 48 8d 35 b2 00 00 00 leaq 178(%rip), %rsi
4e: 48 c7 44 24 10 00 00 00 00 movq $0, 16(%rsp)
57: 48 8d 3d aa 00 00 00 leaq 170(%rip), %rdi
5e: 48 c7 44 24 18 00 00 00 00 movq $0, 24(%rsp)
67: e8 00 00 00 00 callq 0 <_show_inline_in_utils+0x4C>
6c: 48 83 c4 28 addq $40, %rsp
70: c3 retq
71: 66 66 66 66 66 66 2e 0f 1f 84 00 00 00 00 00 nopw %cs:(%rax,%rax)
_show_non_inline_in_utils:
80: 41 54 pushq %r12
82: 49 89 f4 movq %rsi, %r12
85: 55 pushq %rbp
86: 48 89 fd movq %rdi, %rbp
89: 53 pushq %rbx
8a: 48 83 ec 20 subq $32, %rsp
8e: 48 89 e7 movq %rsp, %rdi
91: 48 c7 04 24 00 00 00 00 movq $0, (%rsp)
99: 48 c7 44 24 08 00 00 00 00 movq $0, 8(%rsp)
a2: 48 c7 44 24 10 00 00 00 00 movq $0, 16(%rsp)
ab: 48 c7 44 24 18 00 00 00 00 movq $0, 24(%rsp)
b4: ff d5 callq *%rbp
b6: 48 89 e9 movq %rbp, %rcx
b9: 48 89 e2 movq %rsp, %rdx
bc: 4c 89 e6 movq %r12, %rsi
bf: 48 8d 3d 62 00 00 00 leaq 98(%rip), %rdi
c6: 31 c0 xorl %eax, %eax
c8: e8 00 00 00 00 callq 0 <_show_non_inline_in_utils+0x4D>
cd: 48 83 c4 20 addq $32, %rsp
d1: 5b popq %rbx
d2: 5d popq %rbp
d3: 41 5c popq %r12
d5: c3 retq
d6: 66 2e 0f 1f 84 00 00 00 00 00 nopw %cs:(%rax,%rax)
_show_utils:
e0: 48 83 ec 08 subq $8, %rsp
e4: e8 00 00 00 00 callq 0 <_show_utils+0x9>
e9: 48 8d 35 10 00 00 00 leaq 16(%rip), %rsi
f0: 48 83 c4 08 addq $8, %rsp
f4: 48 8d 3d 00 00 00 00 leaq (%rip), %rdi
fb: e9 00 00 00 00 jmp 0 <_show_utils+0x20>
从中可以发现,show_inline_in_utils已经把get_tu_name内联了,而且还把对入参tu的非空校验去掉了。所以答案是gcc会优先使用内联版本。而且gcc非常聪明,下面这种倒一次手的情况,gcc还是会用内联版本:
void show_utils(void)
{
//show_inline_in_utils();
TU_NAME tu_name = get_tu_name;
char tu[TU_NAME_LEN] = {0};
tu_name(tu);
printf("%s non inline:tu name=%s, func=%p\n", __FILE__, tu, tu_name);
//show_non_inline_in_utils(tu_name, __FILE__);
}
对应的反汇编:
_show_utils:
20: 48 b8 5b 75 74 69 6c 73 5d 00 movabsq $26304082296993115, %rax
2a: 48 83 ec 28 subq $40, %rsp
2e: 48 89 04 24 movq %rax, (%rsp)
32: 48 89 e2 movq %rsp, %rdx
35: 31 c0 xorl %eax, %eax
37: 48 8d 0d 00 00 00 00 leaq (%rip), %rcx
3e: 48 c7 44 24 08 00 00 00 00 movq $0, 8(%rsp)
47: 48 8d 35 ea 00 00 00 leaq 234(%rip), %rsi
4e: 48 c7 44 24 10 00 00 00 00 movq $0, 16(%rsp)
57: 48 8d 3d e2 00 00 00 leaq 226(%rip), %rdi
5e: 48 c7 44 24 18 00 00 00 00 movq $0, 24(%rsp)
67: e8 00 00 00 00 callq 0 <_show_utils+0x4C>
6c: 48 83 c4 28 addq $40, %rsp
70: c3 retq
71: 66 66 66 66 66 66 2e 0f 1f 84 00 00 00 00 00 nopw %cs:(%rax,%rax)
- 为啥非内联版本会选utils.c的版本?
这是标准规定的。C99规定可用下面3种方式的一种来指定哪个源文件来生成内联版本的全局符号。
extern void get_tu_name(char *tu);
void get_tu_name(char *tu);
extern inline void get_tu_name(char *tu);
注意,只需要声明函数,不需要实现。
如果把utils.c的非内联声明删掉,链接时会报错,提示get_tu_name未定义。
如果在main.c也加上非内联声明,链接时又会报重复定义的错误。
C++内联分析
用上面的验证代码,用g++编译有什么效果呢?前面三个问题的答案又是什么呢?
- 内联函数会编译成独立的函数,以及会生成符号吗?
我们先用objdump看看g++编译生成的两个obj和一个bin的符号
in ./main.o:
0000000000000000 gw F __TEXT,__textcoal_nt __Z11get_tu_namePc
in ./utils.o:
00000000000000e0 gw F __TEXT,__textcoal_nt __Z11get_tu_namePc
in test_inline:
0000000100000cb0 gw F __TEXT,__text __Z11get_tu_namePc
最后一列是函数名,get_tu_name经过name mangling处理了。第二列的gw表示符号的类型是全局(global)弱(weak)符号。弱符号就是用来解决不同编译单元的重复定义问题,重复的弱符号在链接时不会报错,链接器会选择一个作为最终的定义。于是,test_inline有且仅有一个定义。
验证代码中main.c和utils.c都引用了get_tu_name的地址,如果main.c不引用函数地址,会怎么样呢?修改代码,编译后的结果如下
in ./main.o:
no get_tu_name found!
in ./utils.o:
00000000000000e0 gw F __TEXT,__textcoal_nt __Z11get_tu_namePc
in test_inline:
0000000100000dd0 gw F __TEXT,__text __Z11get_tu_namePc
main.o不再有get_tu_name的符号了。
我们来看看跟C的差异。相同有两点:
- 内联函数一般情况下会在调用点展开,不会生成符号和独立代码段。
- 没有调用的内联函数不占有目标文件的空间。
不同的地方是源文件引用内联函数地址时,C++会生成一个全局弱符号,而C默认是未定义符号。
- 内联版本和非内联版本同时存在时,编译器会选哪个?
我们把utils.o反汇编看看
__Z11get_tu_namePc:
e0: 48 85 ff testq %rdi, %rdi
e3: 74 0d je 13 <__Z11get_tu_namePc+0x12>
e5: 48 b8 5b 75 74 69 6c 73 5d 00 movabsq $26304082296993115, %rax
ef: 48 89 07 movq %rax, (%rdi)
f2: c3 retq
__Z20show_inline_in_utilsv:
0: 48 b8 5b 75 74 69 6c 73 5d 00 movabsq $26304082296993115, %rax
a: 48 83 ec 28 subq $40, %rsp
e: 48 8b 0d 00 00 00 00 movq (%rip), %rcx
15: 48 89 04 24 movq %rax, (%rsp)
19: 48 89 e2 movq %rsp, %rdx
1c: 31 c0 xorl %eax, %eax
1e: 48 8d 35 d3 00 00 00 leaq 211(%rip), %rsi
25: 48 c7 44 24 08 00 00 00 00 movq $0, 8(%rsp)
2e: 48 8d 3d cb 00 00 00 leaq 203(%rip), %rdi
35: 48 c7 44 24 10 00 00 00 00 movq $0, 16(%rsp)
3e: 48 c7 44 24 18 00 00 00 00 movq $0, 24(%rsp)
47: e8 00 00 00 00 callq 0 <__Z20show_inline_in_utilsv+0x4C>
4c: 48 83 c4 28 addq $40, %rsp
50: c3 retq
51: 66 66 66 66 66 66 2e 0f 1f 84 00 00 00 00 00 nopw %cs:(%rax,%rax)
show_inline_in_utils跟C一样,也把内联函数在调用处展开了,并且也把入参检验去掉了。C++也是优先把内联函数展开,这跟C是一样的。
- 非内联版本会选哪个源文件的版本?
我们看看打印结果
$ make -f Makefile4cpp
g++ -O2 --std=c++11 -DTU="[main]" -c main.c -o main.o
g++ -O2 --std=c++11 -DTU="[utils]" -c utils.c -o utils.o
g++ ./main.o ./utils.o -o test_inline
echo "run test:" && ./test_inline
run test:
main.c inline tu name=[main], func=0x102166cb0
main.c non inline:tu name=[main], func=0x102166cb0
utils.c inline:tu name=[utils], func=0x102166cb0
utils.c non inline:tu name=[main], func=0x102166cb0
从输出可以看出,非内联函数选的是main.c的版本。
如果把obj链接顺序反过来,我们再看看输出
$ make -f Makefile4cpp reversed_build
g++ -O2 --std=c++11 -DTU="[utils]" -c utils.c -o utils.o
g++ -O2 --std=c++11 -DTU="[main]" -c main.c -o main.o
g++ ./utils.o ./main.o -o test_inline
echo "run test:" && ./test_inline
run test:
main.c inline tu name=[main], func=0x100c43d90
main.c non inline:tu name=[utils], func=0x100c43d90
utils.c inline:tu name=[utils], func=0x100c43d90
utils.c non inline:tu name=[utils], func=0x100c43d90
这次,非内联函数又选了utils.c的版本。
链接器会按链接顺序,选择第一个非内联函数版本。
这点是C和C++内联方案最大的不同。第一点的差异也源自这里。这其实源自设计初衷的不同。C++想把内联函数抽象得足够透明,内联函数的选择是编译器实现细节,由编译器搞定,开发者不关心;C正好相反,给开发者开了上帝视角,选择权统统交给开发者。
c99和gnu89的差异
这是C内联最阴暗的角落。在c99引入内联函数的数年前,gcc已经通过标准扩展的方式增加了对内联函数的支持,这包含在gnu89里面。但是两者差异巨大。根本的差别是gnu89的内联函数是全局符号,而c99则不生成符号。
还是用验证代码,utils.h定义的内联函数get_tu_name如下
inline void get_tu_name(char *tu)
{
if (!tu) return;
strcpy(tu, TU);
}
gnu89编译结果如下
$ make -f Makefile4gnu89
gcc -O2 --std=gnu89 -DTU="[main]" -c main.c -o main.o
gcc -O2 --std=gnu89 -DTU="[utils]" -c utils.c -o utils.o
gcc ./main.o ./utils.o -o test_inline
duplicate symbol _get_tu_name in:
./main.o
./utils.o
ld: 1 duplicate symbol for architecture x86_64
collect2: 错误:ld 返回 1
make: *** [ordered_bin] Error 1
$ objdump -t main.o | grep get_tu_name
0000000000000000 g F __TEXT,__text _get_tu_name
$ objdump -t utils.o | grep get_tu_name
0000000000000000 g F __TEXT,__text _get_tu_name
因为utils.h定义了内联函数get_tu_name,而main.c和utils.c都包含了utils.h,结果main.o和utils.o都有此函数的符号。gnu89的内联函数默认会生成全局符号。
一个解决方案是把utils.h定义的get_tu_name加上extern
extern inline void get_tu_name(char *tu)
{
if (!tu) return;
strcpy(tu, TU);
}
编译结果如下
$ make -f Makefile4gnu89
gcc -O2 --std=gnu89 -DTU="[main]" -c main.c -o main.o
gcc -O2 --std=gnu89 -DTU="[utils]" -c utils.c -o utils.o
gcc ./main.o ./utils.o -o test_inline
Undefined symbols for architecture x86_64:
"_get_tu_name", referenced from:
_main in main.o
_show_inline_in_utils in utils.o
_show_utils in utils.o
ld: symbol(s) not found for architecture x86_64
collect2: 错误:ld 返回 1
make: *** [ordered_bin] Error 1
banxia:inline_test yangjia$ objdump -t main.o | grep get_tu_name
0000000000000000 *UND* _get_tu_name
banxia:inline_test yangjia$ objdump -t utils.o | grep get_tu_name
0000000000000000 *UND* _get_tu_name
extern inline无论什么情况都不生成符号,可以解决内联函数重复定义的问题,但是不适用于需要内联函数地址的场景。
还有没有更好的方案呢?gnu89遇到的问题是内联函数是全局的,有没有办法把它变成局部符号呢?躲在角落的static笑了。该他闪亮登场了
static inline void get_tu_name(char *tu)
{
if (!tu) return;
strcpy(tu, TU);
}
再看看编译结果
$ make -f Makefile4gnu89
gcc -O2 --std=gnu89 -DTU="[main]" -c main.c -o main.o
gcc -O2 --std=gnu89 -DTU="[utils]" -c utils.c -o utils.o
gcc ./main.o ./utils.o -o test_inline
echo "run test:" && ./test_inline
run test:
main.c inline tu name=[main], func=0x1037fdc90
main.c non inline:tu name=[main], func=0x1037fdc90
utils.c inline:tu name=[utils], func=0x1037fdcb0
utils.c non inline:tu name=[utils], func=0x1037fdcb0
for inline_symbol in get_tu_name convert2; do for obj in ./main.o ./utils.o test_inline; do echo "in $obj:" && objdump -t $obj | grep $inline_symbol || (echo "no $inline_symbol found!") ; done; done
in ./main.o:
0000000000000000 l F __TEXT,__text _get_tu_name
in ./utils.o:
0000000000000000 l F __TEXT,__text _get_tu_name
in test_inline:
0000000100000c90 l F __TEXT,__text _get_tu_name
0000000100000cb0 l F __TEXT,__text _get_tu_name
重复定义问题总算解决了,但是每个编译单元都有自己的get_tu_name函数实现,不满足内联函数的地址唯一性要求。
static inline似乎是gnu89内联函数最好的方案,于是在早期基于gnu89的代码,大量充斥着在头文件定义的static inline内联函数。
为了跟老代码兼容,用c99编译的代码也有用static inline的情况。但是很不幸的,虽然这种写法在c99也能编译通过,但是gnu89的问题在c99依然存在,内联函数会有多个局部符号,并且打破了函数地址的唯一性。
$ make
gcc -O2 --std=c99 -DTU="[main]" -c main.c -o main.o
gcc -O2 --std=c99 -DTU="[utils]" -c utils.c -o utils.o
gcc ./main.o ./utils.o -o test_inline
echo "run test:" && ./test_inline
run test:
main.c inline tu name=[main], func=0x108e30c90
main.c non inline:tu name=[main], func=0x108e30c90
utils.c inline:tu name=[utils], func=0x108e30cb0
utils.c non inline:tu name=[utils], func=0x108e30cb0
for inline_symbol in get_tu_name convert2; do for obj in ./main.o ./utils.o test_inline; do echo "in $obj:" && objdump -t $obj | grep $inline_symbol || (echo "no $inline_symbol found!") ; done; done
in ./main.o:
0000000000000000 l F __TEXT,__text _get_tu_name
in ./utils.o:
0000000000000000 l F __TEXT,__text _get_tu_name
in test_inline:
0000000100000c90 l F __TEXT,__text _get_tu_name
0000000100000cb0 l F __TEXT,__text _get_tu_name
gcc内联相关的优化开关
内联是一种编译阶段的优化手段。这有两层含义:
- 内联发生在编译阶段,而C/C++每个源文件是相互独立的编译单元。所以内联受限于单个源文件的编译,内联函数的定义必须在单个编译单元内部可见。如果在某个源文件只声明函数为内联,而函数定义放到其它源文件,内联不会生效。内联如果需要在跨源文件共享,推荐解决方案是把内联的声明和实现都放在头文件中。
- 内联依赖于优化开关。不同的优化级别下,内联够的效果不尽相同。
下面以gcc为例,逐个分析各个优化级别对内联的影响。
- O0: 目标是减少编译时间并生成debug信息。相当于打开no-inline。除标记了always_inline外的函数内联不生效,在调用点不展开。
- O1: 目标是减少代码大小和其执行时间。但不进行非常耗时的优化。编译时间会变长,但不会长得太离谱。这个优化等级会打开内联优化开关inline-functions-called-once。
这个开关控制使能静态函数的内联推导,而无论函数有没有声明成内联。如果静态函数在所有调用点都内联了,这个函数其实就没有存在的必要了,编译器会把它从obj中去掉。 - O2: 在O1基础上继续优化。除了包含O1所有优化选项,还增加另外优化选项。跟内联有关的增加了inline-small-functions、indirect-inlining和partial-inlining。
-- inline-small-functions: 当函数体代码长度比编译器生成的函数调用代码更短时,函数在调用点自动展开。注意,这不仅仅针对内联函数,而是所有函数。就算源码中没有标上inline的函数也会参与此优化。
-- indirect-inlining: 间接调用的内联函数也会在调用点展开。这个开关有两个限制,一是只针对编译时可见的间接调用,跨源文件的间接调用不在考虑之列;二是依赖于内联功能的使能(先要打开inline-functions或者inline-small-functions )
-- partial-inlining: 内联函数的局部代码,前提是内联使能。详见这里 - O3: 基于O2继续优化,内联优化选项又加了个inline-functions。
这个选项把所有函数都纳入内联推导,而不管函数有没有声明为内联。当然,推导是过程,并不能保证内联(调用点扩展)一定会成功。
除此之外,这个选项还包括对静态函数的优化,效果跟inline-functions-called-once一样。如果静态函数在所有调用点都内联了,编译器会把它从obj中去掉。
上面列出来的各个优化级别的内联开关是基于本地环境的,可能跟你的环境配置不同。gcc提供了查询各个级别优化选项是否使能的命令参数-Q --help=optimizers。例如,如下是查询O2下,各个优化选项使能与否的列表
gcc -O2 -Q --help=optimizers
- 默认打开的选项
-- early-inlining: 针对两类函数(标记为always_inline的函数和函数执行体比调用开销小的函数),在编译器执行内联分析的一趟前完成内联。
最后再看看内联什么情况下会失效
- 优化开关没打开
- 打开no-inline选项。注意,这对标记为always_inline的函数无效。
- 打开keep-inline-functions选项。就算函数在所有调用点都内联了,obj也会包含其独立的代码段。
- 函数过长
- 下面几种函数定义也会导致内联失效
函数参数是可变的
函数调用了alloca
函数使用了computed goto或nonlocal goto
函数是嵌套函数。并不是嵌套就一定不能内联。gcc专门提供优化选项控制嵌套深度和内联展开后的最大IR数,只要满足条件就可以内联。
函数使用了__builtin_longjmp、__builtin_return、__builtin_apply_args
内联还有一个编译告警开关Winline。打开这个开关后,当内联函数不能展开时,会报编译告警,并会提示失败原因。
gcc把内联分成两个部分: 内联展开和去符号化。前者在标准中有明确定义(好吧,C99正文只提到“Making a function an inline function suggests that calls to the function be as fast as possible”,内联实际上是由编译器实现决定的)。后者则是编译器自身众多优化方式之一。就算在所有调用点,函数都内联展开了,函数并不一定会去符号化。
从gcc支持情况来看,内联似乎跟Dr.Dobb预测的一样,会像另一个C关键字register一样,最终在历史长河中化为乌有乡的一员。
BTW: 好吧,更正一下。register并没有完全被无视。这个关键字最原始的作用(请求编译器将局部变量放在寄存器中)已经没太大意义了,但它的衍生功能(禁止别名,即不能访问register变量的地址)还有不小的使用场景。
附
- 完整代码和makefile见github仓
- BTW,内联又有新成员入坑。C++17引入了内联变量,有兴趣可以看看cppreference