浅析C/C++内联函数

内联的疑惑

写这篇文章的初衷源自于对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_nameshow_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])。
这里有三个问题:

  1. 内联函数会编译成独立的函数,以及会生成符号吗?
    我们先看看两个obj的符号表有没有get_tu_name
in ./main.o:
0000000000000000         *UND*  _get_tu_name
in ./utils.o:
0000000000000000 g     F __TEXT,__text  _get_tu_name

其中,utils.oget_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,有兴趣可以编译看看:)
综上所述,内联函数一般情况下会在调用点展开,不会生成符号和独立代码段。而且没有调用的内联函数不占有目标文件的空间。特殊情况是指编译器驳回了内联请求,不将函数内联。

  1. 内联版本和非内联版本同时存在时,编译器会选哪个?
    标准其实没有规定,留给编译器自己决定。所以在回答这个问题前,我们先看看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)
  1. 为啥非内联版本会选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++编译有什么效果呢?前面三个问题的答案又是什么呢?

  1. 内联函数会编译成独立的函数,以及会生成符号吗?
    我们先用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默认是未定义符号。

  1. 内联版本和非内联版本同时存在时,编译器会选哪个?
    我们把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是一样的。

  1. 非内联版本会选哪个源文件的版本?
    我们看看打印结果
$ 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-functionsindirect-inliningpartial-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 gotononlocal 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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,583评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,669评论 2 374
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,684评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,682评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,533评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,396评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,814评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,458评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,745评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,789评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,565评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,410评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,828评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,050评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,342评论 1 253
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,793评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,010评论 2 337

推荐阅读更多精彩内容