浅析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阅读 156,907评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,546评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,705评论 0 238
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,624评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,940评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,371评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,672评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,396评论 0 195
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,069评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,350评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,876评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,243评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,847评论 3 231
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,004评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,755评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,378评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,266评论 2 259

推荐阅读更多精彩内容

  • 1.ios高性能编程 (1).内层 最小的内层平均值和峰值(2).耗电量 高效的算法和数据结构(3).初始化时...
    欧辰_OSR阅读 29,012评论 8 265
  • 一、温故而知新 1. 内存不够怎么办 内存简单分配策略的问题地址空间不隔离内存使用效率低程序运行的地址不确定 关于...
    SeanCST阅读 7,663评论 0 27
  • 过周末了,当然是周五就开启了假期模式。刚追完剧,所以今天打算分享几张拍摄的照片啦。一起来感受一下有爱的家庭氛围吧。
    Michiko七七阅读 365评论 0 0
  • 一直以来,我总是吝啬对学生进行表扬,特别是对一些总没进步的学生,缺乏足够的耐心,心里觉得这些学生怎么屡教不改,...
    两杠四星阅读 521评论 3 4
  • 苍茫大地任独行,策马扬鞭百丈弓。 稼穑方知甘伴苦,躬耕解晓雨缠风。 不辞劳累朝夕事,难忘饥寒夙夜功。 感念青春来务...
    不惑而歌阅读 995评论 35 39