淺析C/C++內(nèi)聯(lián)函數(shù)

內(nèi)聯(lián)的疑惑

寫(xiě)這篇文章的初衷源自于對(duì)netdata項(xiàng)目把C函數(shù)聲明為static inline的用法不解。從語(yǔ)言特性上看,內(nèi)聯(lián)函數(shù)在編譯時(shí)展開(kāi),本來(lái)就沒(méi)有符號(hào),再加個(gè)static不是多此一舉嗎?更有甚者,netdata對(duì)大部的C函數(shù)都聲明為static inline(號(hào)稱是為了追求極致的執(zhí)行性能,個(gè)人存疑),難道沒(méi)有優(yōu)化開(kāi)關(guān)強(qiáng)制編譯器對(duì)每個(gè)函數(shù)都進(jìn)行內(nèi)聯(lián)推導(dǎo)嗎?
而后在整改老代碼過(guò)程中,需要把復(fù)雜的仿函數(shù)宏重構(gòu)成內(nèi)聯(lián)函數(shù)。其間發(fā)現(xiàn)很多同學(xué)對(duì)內(nèi)聯(lián)也不甚了解(me too:()。希望這篇文章可以撥開(kāi)迷霧,幫助大家理清內(nèi)聯(lián)的細(xì)節(jié)。

在“知新”之前,我們先“溫故”。先把時(shí)間坐標(biāo)前移到上個(gè)世紀(jì)末。C99就在那個(gè)時(shí)候問(wèn)世。在C99新增的特性中,有一個(gè)來(lái)自于C++。這就是本文的主角內(nèi)聯(lián)函數(shù)。C不是無(wú)腦的拿來(lái)主義,除了語(yǔ)言特性和限制不同之外,在實(shí)現(xiàn)細(xì)節(jié)上跟C++有較大不同。最大的差異在于內(nèi)聯(lián)函數(shù)地址的選擇。
內(nèi)聯(lián)函數(shù)有一個(gè)很重要的特性,在所有源文件引用的內(nèi)聯(lián)函數(shù)地址是一樣的。這點(diǎn),C和C++都滿足。但是實(shí)現(xiàn)方式卻有所不同。
下面通過(guò)一個(gè)實(shí)驗(yàn)來(lái)展開(kāi)兩者差異的分析。

驗(yàn)證代碼說(shuō)明

如下所示,在頭文件utils.h中定義了一個(gè)內(nèi)聯(lián)函數(shù)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除了分別以內(nèi)聯(lián)的方式和非內(nèi)聯(lián)的方式調(diào)用get_tu_name,也引用其函數(shù)地址。如下所示:

#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;
}

在另外一個(gè)源文件utils.c也會(huì)做同樣的事。show_inline_in_utils以內(nèi)聯(lián)方式調(diào)用get_tu_name,show_non_inline_in_utils以非內(nèi)聯(lián)方式調(diào)用,這兩個(gè)函數(shù)都引用了函數(shù)地址:

#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);
}

為了測(cè)試內(nèi)聯(lián)函數(shù)是從屬于哪個(gè)源文件,get_tu_name引用的TU是一個(gè)編譯宏,其在makefile的編譯規(guī)則中指定,如下的-DTU=XXX

%.o: %.c
    $(CC) $(OPTIM) --std=$(STD) -DTU=\"$(patsubst %.c,[%],$<)\" -c $< -o $@

main.c對(duì)應(yīng)的TU[main],utils.c則為[utils]。

驗(yàn)證環(huán)境:

  • 編譯器版本是gcc6.2.0
  • OS是64位的MacOS 10.13.5

C內(nèi)聯(lián)分析(c99)

我們先看看gcc以C99標(biāo)準(zhǔn)編譯后的輸出:

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

無(wú)論是內(nèi)聯(lián)還是非內(nèi)聯(lián),get_tu_name的地址都是一樣的。內(nèi)聯(lián)和非內(nèi)聯(lián)的差異在于內(nèi)聯(lián)使用的是源文件各自的版本,而非內(nèi)聯(lián)使用的是utils.c的版本(TU[utils])。
這里有三個(gè)問(wèn)題:

  1. 內(nèi)聯(lián)函數(shù)會(huì)編譯成獨(dú)立的函數(shù),以及會(huì)生成符號(hào)嗎?
    我們先看看兩個(gè)obj的符號(hào)表有沒(méi)有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的全局符號(hào),因?yàn)樗贿x中為內(nèi)聯(lián)函數(shù)的非內(nèi)聯(lián)版本的提供者(見(jiàn)第3點(diǎn))。而main.o則是未定義符號(hào),main.o沒(méi)有生成符號(hào),反而依賴于外部的get_tu_name。從這里可以看出內(nèi)聯(lián)函數(shù)沒(méi)有全局符號(hào)。
從第二點(diǎn)的反匯編來(lái)看,show_inline_in_utils在調(diào)用get_tu_name的地方已經(jīng)像宏一樣展開(kāi)了,而main.o在調(diào)用點(diǎn)展開(kāi)后,get_tu_name既沒(méi)有符號(hào),也沒(méi)有獨(dú)立夠的函數(shù)代碼段。
更有甚者,如果內(nèi)聯(lián)函數(shù)在源文件沒(méi)有引用,目標(biāo)文件沒(méi)有內(nèi)聯(lián)函數(shù)的半點(diǎn)信息,就像代碼里面就沒(méi)有內(nèi)聯(lián)函數(shù)一樣。如果沒(méi)有內(nèi)聯(lián)函數(shù),我們只有用仿函數(shù)宏或者靜態(tài)函數(shù)來(lái)達(dá)到這個(gè)目的。utils.h還有另外一個(gè)沒(méi)人調(diào)用的內(nèi)聯(lián)函數(shù)convert2,有興趣可以編譯看看:)
綜上所述,內(nèi)聯(lián)函數(shù)一般情況下會(huì)在調(diào)用點(diǎn)展開(kāi),不會(huì)生成符號(hào)和獨(dú)立代碼段。而且沒(méi)有調(diào)用的內(nèi)聯(lián)函數(shù)不占有目標(biāo)文件的空間。特殊情況是指編譯器駁回了內(nèi)聯(lián)請(qǐng)求,不將函數(shù)內(nèi)聯(lián)。

  1. 內(nèi)聯(lián)版本和非內(nèi)聯(lián)版本同時(shí)存在時(shí),編譯器會(huì)選哪個(gè)?
    標(biāo)準(zhǔn)其實(shí)沒(méi)有規(guī)定,留給編譯器自己決定。所以在回答這個(gè)問(wèn)題前,我們先看看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>

從中可以發(fā)現(xiàn),show_inline_in_utils已經(jīng)把get_tu_name內(nèi)聯(lián)了,而且還把對(duì)入?yún)?em>tu的非空校驗(yàn)去掉了。所以答案是gcc會(huì)優(yōu)先使用內(nèi)聯(lián)版本。而且gcc非常聰明,下面這種倒一次手的情況,gcc還是會(huì)用內(nèi)聯(lián)版本:

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__);
}

對(duì)應(yīng)的反匯編:

_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. 為啥非內(nèi)聯(lián)版本會(huì)選utils.c的版本?
    這是標(biāo)準(zhǔn)規(guī)定的。C99規(guī)定可用下面3種方式的一種來(lái)指定哪個(gè)源文件來(lái)生成內(nèi)聯(lián)版本的全局符號(hào)。
extern void get_tu_name(char *tu);
void get_tu_name(char *tu);
extern inline void get_tu_name(char *tu);

注意,只需要聲明函數(shù),不需要實(shí)現(xiàn)。
如果把utils.c的非內(nèi)聯(lián)聲明刪掉,鏈接時(shí)會(huì)報(bào)錯(cuò),提示get_tu_name未定義。
如果在main.c也加上非內(nèi)聯(lián)聲明,鏈接時(shí)又會(huì)報(bào)重復(fù)定義的錯(cuò)誤。

C++內(nèi)聯(lián)分析

用上面的驗(yàn)證代碼,用g++編譯有什么效果呢?前面三個(gè)問(wèn)題的答案又是什么呢?

  1. 內(nèi)聯(lián)函數(shù)會(huì)編譯成獨(dú)立的函數(shù),以及會(huì)生成符號(hào)嗎?
    我們先用objdump看看g++編譯生成的兩個(gè)obj和一個(gè)bin的符號(hào)
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

最后一列是函數(shù)名,get_tu_name經(jīng)過(guò)name mangling處理了。第二列的gw表示符號(hào)的類型是全局(global)弱(weak)符號(hào)。弱符號(hào)就是用來(lái)解決不同編譯單元的重復(fù)定義問(wèn)題,重復(fù)的弱符號(hào)在鏈接時(shí)不會(huì)報(bào)錯(cuò),鏈接器會(huì)選擇一個(gè)作為最終的定義。于是,test_inline有且僅有一個(gè)定義。
驗(yàn)證代碼中main.c和utils.c都引用了get_tu_name的地址,如果main.c不引用函數(shù)地址,會(huì)怎么樣呢?修改代碼,編譯后的結(jié)果如下

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的符號(hào)了。
我們來(lái)看看跟C的差異。相同有兩點(diǎn):

  • 內(nèi)聯(lián)函數(shù)一般情況下會(huì)在調(diào)用點(diǎn)展開(kāi),不會(huì)生成符號(hào)和獨(dú)立代碼段。
  • 沒(méi)有調(diào)用的內(nèi)聯(lián)函數(shù)不占有目標(biāo)文件的空間。

不同的地方是源文件引用內(nèi)聯(lián)函數(shù)地址時(shí),C++會(huì)生成一個(gè)全局弱符號(hào),而C默認(rèn)是未定義符號(hào)。

  1. 內(nèi)聯(lián)版本和非內(nèi)聯(lián)版本同時(shí)存在時(shí),編譯器會(huì)選哪個(gè)?
    我們把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一樣,也把內(nèi)聯(lián)函數(shù)在調(diào)用處展開(kāi)了,并且也把入?yún)z驗(yàn)去掉了。C++也是優(yōu)先把內(nèi)聯(lián)函數(shù)展開(kāi),這跟C是一樣的。

  1. 非內(nèi)聯(lián)版本會(huì)選哪個(gè)源文件的版本?
    我們看看打印結(jié)果
$ 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

從輸出可以看出,非內(nèi)聯(lián)函數(shù)選的是main.c的版本。
如果把obj鏈接順序反過(guò)來(lái),我們?cè)倏纯摧敵?/p>

$ 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

這次,非內(nèi)聯(lián)函數(shù)又選了utils.c的版本。
鏈接器會(huì)按鏈接順序,選擇第一個(gè)非內(nèi)聯(lián)函數(shù)版本。
這點(diǎn)是C和C++內(nèi)聯(lián)方案最大的不同。第一點(diǎn)的差異也源自這里。這其實(shí)源自設(shè)計(jì)初衷的不同。C++想把內(nèi)聯(lián)函數(shù)抽象得足夠透明,內(nèi)聯(lián)函數(shù)的選擇是編譯器實(shí)現(xiàn)細(xì)節(jié),由編譯器搞定,開(kāi)發(fā)者不關(guān)心;C正好相反,給開(kāi)發(fā)者開(kāi)了上帝視角,選擇權(quán)統(tǒng)統(tǒng)交給開(kāi)發(fā)者。

c99和gnu89的差異

這是C內(nèi)聯(lián)最陰暗的角落。在c99引入內(nèi)聯(lián)函數(shù)的數(shù)年前,gcc已經(jīng)通過(guò)標(biāo)準(zhǔn)擴(kuò)展的方式增加了對(duì)內(nèi)聯(lián)函數(shù)的支持,這包含在gnu89里面。但是兩者差異巨大。根本的差別是gnu89的內(nèi)聯(lián)函數(shù)是全局符號(hào),而c99則不生成符號(hào)。
還是用驗(yàn)證代碼,utils.h定義的內(nèi)聯(lián)函數(shù)get_tu_name如下

inline void get_tu_name(char *tu)
{
    if (!tu) return;
    strcpy(tu, TU);
}

gnu89編譯結(jié)果如下

$ 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: 錯(cuò)誤: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

因?yàn)閡tils.h定義了內(nèi)聯(lián)函數(shù)get_tu_name,而main.c和utils.c都包含了utils.h,結(jié)果main.o和utils.o都有此函數(shù)的符號(hào)。gnu89的內(nèi)聯(lián)函數(shù)默認(rèn)會(huì)生成全局符號(hào)。
一個(gè)解決方案是把utils.h定義的get_tu_name加上extern

extern inline void get_tu_name(char *tu)
{
    if (!tu) return;
    strcpy(tu, TU);
}

編譯結(jié)果如下

$ 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: 錯(cuò)誤: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無(wú)論什么情況都不生成符號(hào),可以解決內(nèi)聯(lián)函數(shù)重復(fù)定義的問(wèn)題,但是不適用于需要內(nèi)聯(lián)函數(shù)地址的場(chǎng)景。
還有沒(méi)有更好的方案呢?gnu89遇到的問(wèn)題是內(nèi)聯(lián)函數(shù)是全局的,有沒(méi)有辦法把它變成局部符號(hào)呢?躲在角落的static笑了。該他閃亮登場(chǎng)了

static inline void get_tu_name(char *tu)
{
    if (!tu) return;
    strcpy(tu, TU);
}

再看看編譯結(jié)果

$ 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

重復(fù)定義問(wèn)題總算解決了,但是每個(gè)編譯單元都有自己的get_tu_name函數(shù)實(shí)現(xiàn),不滿足內(nèi)聯(lián)函數(shù)的地址唯一性要求。
static inline似乎是gnu89內(nèi)聯(lián)函數(shù)最好的方案,于是在早期基于gnu89的代碼,大量充斥著在頭文件定義的static inline內(nèi)聯(lián)函數(shù)。
為了跟老代碼兼容,用c99編譯的代碼也有用static inline的情況。但是很不幸的,雖然這種寫(xiě)法在c99也能編譯通過(guò),但是gnu89的問(wèn)題在c99依然存在,內(nèi)聯(lián)函數(shù)會(huì)有多個(gè)局部符號(hào),并且打破了函數(shù)地址的唯一性。

$ 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內(nèi)聯(lián)相關(guān)的優(yōu)化開(kāi)關(guān)

內(nèi)聯(lián)是一種編譯階段的優(yōu)化手段。這有兩層含義:

  • 內(nèi)聯(lián)發(fā)生在編譯階段,而C/C++每個(gè)源文件是相互獨(dú)立的編譯單元。所以內(nèi)聯(lián)受限于單個(gè)源文件的編譯,內(nèi)聯(lián)函數(shù)的定義必須在單個(gè)編譯單元內(nèi)部可見(jiàn)。如果在某個(gè)源文件只聲明函數(shù)為內(nèi)聯(lián),而函數(shù)定義放到其它源文件,內(nèi)聯(lián)不會(huì)生效。內(nèi)聯(lián)如果需要在跨源文件共享,推薦解決方案是把內(nèi)聯(lián)的聲明和實(shí)現(xiàn)都放在頭文件中。
  • 內(nèi)聯(lián)依賴于優(yōu)化開(kāi)關(guān)。不同的優(yōu)化級(jí)別下,內(nèi)聯(lián)夠的效果不盡相同。

下面以gcc為例,逐個(gè)分析各個(gè)優(yōu)化級(jí)別對(duì)內(nèi)聯(lián)的影響。

  • O0: 目標(biāo)是減少編譯時(shí)間并生成debug信息。相當(dāng)于打開(kāi)no-inline。除標(biāo)記了always_inline外的函數(shù)內(nèi)聯(lián)不生效,在調(diào)用點(diǎn)不展開(kāi)。
  • O1: 目標(biāo)是減少代碼大小和其執(zhí)行時(shí)間。但不進(jìn)行非常耗時(shí)的優(yōu)化。編譯時(shí)間會(huì)變長(zhǎng),但不會(huì)長(zhǎng)得太離譜。這個(gè)優(yōu)化等級(jí)會(huì)打開(kāi)內(nèi)聯(lián)優(yōu)化開(kāi)關(guān)inline-functions-called-once。
    這個(gè)開(kāi)關(guān)控制使能靜態(tài)函數(shù)的內(nèi)聯(lián)推導(dǎo),而無(wú)論函數(shù)有沒(méi)有聲明成內(nèi)聯(lián)。如果靜態(tài)函數(shù)在所有調(diào)用點(diǎn)都內(nèi)聯(lián)了,這個(gè)函數(shù)其實(shí)就沒(méi)有存在的必要了,編譯器會(huì)把它從obj中去掉。
  • O2: 在O1基礎(chǔ)上繼續(xù)優(yōu)化。除了包含O1所有優(yōu)化選項(xiàng),還增加另外優(yōu)化選項(xiàng)。跟內(nèi)聯(lián)有關(guān)的增加了inline-small-functions、indirect-inliningpartial-inlining
    -- inline-small-functions: 當(dāng)函數(shù)體代碼長(zhǎng)度比編譯器生成的函數(shù)調(diào)用代碼更短時(shí),函數(shù)在調(diào)用點(diǎn)自動(dòng)展開(kāi)。注意,這不僅僅針對(duì)內(nèi)聯(lián)函數(shù),而是所有函數(shù)。就算源碼中沒(méi)有標(biāo)上inline的函數(shù)也會(huì)參與此優(yōu)化。
    -- indirect-inlining: 間接調(diào)用的內(nèi)聯(lián)函數(shù)也會(huì)在調(diào)用點(diǎn)展開(kāi)。這個(gè)開(kāi)關(guān)有兩個(gè)限制,一是只針對(duì)編譯時(shí)可見(jiàn)的間接調(diào)用,跨源文件的間接調(diào)用不在考慮之列;二是依賴于內(nèi)聯(lián)功能的使能(先要打開(kāi)inline-functions或者inline-small-functions )
    -- partial-inlining: 內(nèi)聯(lián)函數(shù)的局部代碼,前提是內(nèi)聯(lián)使能。詳見(jiàn)這里
  • O3: 基于O2繼續(xù)優(yōu)化,內(nèi)聯(lián)優(yōu)化選項(xiàng)又加了個(gè)inline-functions。
    這個(gè)選項(xiàng)把所有函數(shù)都納入內(nèi)聯(lián)推導(dǎo),而不管函數(shù)有沒(méi)有聲明為內(nèi)聯(lián)。當(dāng)然,推導(dǎo)是過(guò)程,并不能保證內(nèi)聯(lián)(調(diào)用點(diǎn)擴(kuò)展)一定會(huì)成功。
    除此之外,這個(gè)選項(xiàng)還包括對(duì)靜態(tài)函數(shù)的優(yōu)化,效果跟inline-functions-called-once一樣。如果靜態(tài)函數(shù)在所有調(diào)用點(diǎn)都內(nèi)聯(lián)了,編譯器會(huì)把它從obj中去掉。
    上面列出來(lái)的各個(gè)優(yōu)化級(jí)別的內(nèi)聯(lián)開(kāi)關(guān)是基于本地環(huán)境的,可能跟你的環(huán)境配置不同。gcc提供了查詢各個(gè)級(jí)別優(yōu)化選項(xiàng)是否使能的命令參數(shù)-Q --help=optimizers。例如,如下是查詢O2下,各個(gè)優(yōu)化選項(xiàng)使能與否的列表
gcc -O2 -Q --help=optimizers
  • 默認(rèn)打開(kāi)的選項(xiàng)
    -- early-inlining: 針對(duì)兩類函數(shù)(標(biāo)記為always_inline的函數(shù)和函數(shù)執(zhí)行體比調(diào)用開(kāi)銷小的函數(shù)),在編譯器執(zhí)行內(nèi)聯(lián)分析的一趟前完成內(nèi)聯(lián)。

最后再看看內(nèi)聯(lián)什么情況下會(huì)失效

  • 優(yōu)化開(kāi)關(guān)沒(méi)打開(kāi)
  • 打開(kāi)no-inline選項(xiàng)。注意,這對(duì)標(biāo)記為always_inline的函數(shù)無(wú)效。
  • 打開(kāi)keep-inline-functions選項(xiàng)。就算函數(shù)在所有調(diào)用點(diǎn)都內(nèi)聯(lián)了,obj也會(huì)包含其獨(dú)立的代碼段。
  • 函數(shù)過(guò)長(zhǎng)
  • 下面幾種函數(shù)定義也會(huì)導(dǎo)致內(nèi)聯(lián)失效
    函數(shù)參數(shù)是可變的
    函數(shù)調(diào)用了alloca
    函數(shù)使用了computed gotononlocal goto
    函數(shù)是嵌套函數(shù)。并不是嵌套就一定不能內(nèi)聯(lián)。gcc專門(mén)提供優(yōu)化選項(xiàng)控制嵌套深度和內(nèi)聯(lián)展開(kāi)后的最大IR數(shù),只要滿足條件就可以內(nèi)聯(lián)。
    函數(shù)使用了__builtin_longjmp__builtin_return、__builtin_apply_args

內(nèi)聯(lián)還有一個(gè)編譯告警開(kāi)關(guān)Winline。打開(kāi)這個(gè)開(kāi)關(guān)后,當(dāng)內(nèi)聯(lián)函數(shù)不能展開(kāi)時(shí),會(huì)報(bào)編譯告警,并會(huì)提示失敗原因。

gcc把內(nèi)聯(lián)分成兩個(gè)部分: 內(nèi)聯(lián)展開(kāi)和去符號(hào)化。前者在標(biāo)準(zhǔn)中有明確定義(好吧,C99正文只提到“Making a function an inline function suggests that calls to the function be as fast as possible”,內(nèi)聯(lián)實(shí)際上是由編譯器實(shí)現(xiàn)決定的)。后者則是編譯器自身眾多優(yōu)化方式之一。就算在所有調(diào)用點(diǎn),函數(shù)都內(nèi)聯(lián)展開(kāi)了,函數(shù)并不一定會(huì)去符號(hào)化。

從gcc支持情況來(lái)看,內(nèi)聯(lián)似乎跟Dr.Dobb預(yù)測(cè)的一樣,會(huì)像另一個(gè)C關(guān)鍵字register一樣,最終在歷史長(zhǎng)河中化為烏有鄉(xiāng)的一員。

BTW: 好吧,更正一下。register并沒(méi)有完全被無(wú)視。這個(gè)關(guān)鍵字最原始的作用(請(qǐng)求編譯器將局部變量放在寄存器中)已經(jīng)沒(méi)太大意義了,但它的衍生功能(禁止別名,即不能訪問(wèn)register變量的地址)還有不小的使用場(chǎng)景。

  • 完整代碼和makefile見(jiàn)github倉(cāng)
  • BTW,內(nèi)聯(lián)又有新成員入坑。C++17引入了內(nèi)聯(lián)變量,有興趣可以看看cppreference
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容