內(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)題:
- 內(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.o有get_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)。
- 內(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)
- 為啥非內(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)題的答案又是什么呢?
- 內(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)。
- 內(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是一樣的。
- 非內(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-inlining和partial-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 goto或nonlocal 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