這一節(jié),接著講 attribute 屬性聲明,attribute可以說(shuō)是 GNU C 最大的特色。我們接下來(lái)繼續(xù)講一下跟內(nèi)聯(lián)函數(shù)相關(guān)的兩個(gè)屬性:noinline 和 always_inline。這兩個(gè)屬性的用途是告訴編譯器:編譯時(shí),對(duì)我們指定的函數(shù)內(nèi)聯(lián)展開(kāi)或不展開(kāi)。它們的使用方法如下。
成都創(chuàng)新互聯(lián)公司2013年開(kāi)創(chuàng)至今,先為靈璧等服務(wù)建站,靈璧等地企業(yè),進(jìn)行企業(yè)商務(wù)咨詢服務(wù)。為靈璧企業(yè)網(wǎng)站制作PC+手機(jī)+微官網(wǎng)三網(wǎng)同步一站式服務(wù)解決您的所有建站問(wèn)題。
static inline __attribute__((noinline)) int func();
static inline __attribute__((always_inline)) int func();
內(nèi)聯(lián)函數(shù)使用 inline 聲明即可,有時(shí)候還會(huì)用 static 和 extern 修飾。使用 inline 聲明一個(gè)內(nèi)聯(lián)函數(shù),和使用關(guān)鍵字 register 聲明一個(gè)變量一樣,只是建議編譯器在編譯時(shí)內(nèi)聯(lián)展開(kāi)。使用關(guān)鍵字 register 修飾變量時(shí),只是建議編譯器在給變量分配存儲(chǔ)空間時(shí),將這個(gè)變量放到寄存器里,這樣,程序的運(yùn)行效率會(huì)更高。那編譯器會(huì)不會(huì)放呢?編譯器就要根據(jù)寄存器資源緊不緊張,這個(gè)變量用得頻不頻繁來(lái)做權(quán)衡。
同樣,當(dāng)一個(gè)函數(shù)使用 inline 關(guān)鍵字修飾,編譯器在編譯時(shí)一定會(huì)內(nèi)聯(lián)展開(kāi)嗎?未必。編譯器也會(huì)根據(jù)實(shí)際情況,比如函數(shù)體大小、函數(shù)體內(nèi)是否有循環(huán)結(jié)構(gòu)、是否有指針、是否有遞歸、函數(shù)調(diào)用是否頻繁來(lái)做決定。比如 GCC 編譯器,一般是不會(huì)對(duì)內(nèi)聯(lián)函數(shù)展開(kāi)的,只有當(dāng)編譯優(yōu)化選項(xiàng)開(kāi)到 -O2 以上,才會(huì)考慮是否內(nèi)聯(lián)展開(kāi)。當(dāng)我們使用 noinline 和 always_inline 對(duì)一個(gè)內(nèi)聯(lián)函數(shù)作了屬性聲明后,編譯器的編譯行為就變得確定了。使用 noinline 聲明,就是告訴編譯器,不要展開(kāi);使用 always_inline 屬性聲明,就是告訴編譯器,要內(nèi)聯(lián)展開(kāi)。
什么是內(nèi)聯(lián)展開(kāi)呢?我們不得不說(shuō)一下內(nèi)聯(lián)函數(shù)的基礎(chǔ)知識(shí)。
說(shuō)起內(nèi)聯(lián)函數(shù),又不得不說(shuō)函數(shù)調(diào)用開(kāi)銷。一個(gè)函數(shù)在執(zhí)行過(guò)程中,如果需要調(diào)用其它函數(shù),一般會(huì)執(zhí)行下面這個(gè)過(guò)程。
比如一個(gè) ARM 程序,在一個(gè)函數(shù) f1() 中,我們對(duì)一些數(shù)據(jù)進(jìn)行處理,運(yùn)算結(jié)果暫時(shí)保存在 R0 寄存器中。接著要調(diào)用另外一個(gè)函數(shù) f2(),調(diào)用結(jié)束后,接著返回到 f1() 函數(shù)中繼續(xù)處理數(shù)據(jù)。如果我們?cè)?f2() 函數(shù)中使用到 R0 這個(gè)寄存器(用于保存函數(shù)的返回值),此時(shí)就會(huì)改變 R0 寄存器中的值,那么就篡改了 f1() 函數(shù)中的暫存運(yùn)算結(jié)果。當(dāng)我們返回到 f1() 函數(shù)中繼續(xù)進(jìn)行運(yùn)算時(shí),結(jié)果肯定不正確。
那怎么辦呢?很簡(jiǎn)單,在跳到 f2() 執(zhí)行之前,先把 R0 寄存器的值保存到堆棧中,f() 函數(shù)執(zhí)行結(jié)束后,再將堆棧中的值恢復(fù)到 R0 寄存器中,這樣 f1() 函數(shù)就可以接著繼續(xù)執(zhí)行了,就跟什么事情都沒(méi)發(fā)生過(guò)一樣。
這種方法證明是 OK 的,現(xiàn)代計(jì)算機(jī)系統(tǒng),無(wú)論是什么架構(gòu)和指令集,都是采用這種方法。雖然麻煩了點(diǎn),但至少能解決問(wèn)題,無(wú)非就是多花點(diǎn)代價(jià),需要不斷地保存現(xiàn)場(chǎng)、恢復(fù)現(xiàn)場(chǎng),這就是函數(shù)調(diào)用帶來(lái)的開(kāi)銷。
對(duì)于一般的函數(shù)調(diào)用,這種方法是沒(méi)有問(wèn)題的。但對(duì)于一些極端情況,比如說(shuō)一個(gè)函數(shù)很小,函數(shù)體內(nèi)只有一行代碼,而且被大量頻繁的調(diào)用。如果每次調(diào)用,都不斷地保存現(xiàn)場(chǎng),執(zhí)行時(shí)卻發(fā)現(xiàn)函數(shù)只有一行代碼,又要恢復(fù)現(xiàn)場(chǎng),往往造成函數(shù)開(kāi)銷比較大,性價(jià)比不高。這就跟你去五星級(jí)飯店訂個(gè)餐位吃飯一樣,VIP 包間、刀叉餐具、空調(diào)、服務(wù)人員都準(zhǔn)備好了,你到了之后只要了一碗面條,吃完之后抹嘴走人,而且一天三頓你都這么干,你說(shuō)服務(wù)員煩不煩?
函數(shù)調(diào)用也是如此。有些函數(shù)很小,而且調(diào)用頻繁,調(diào)用開(kāi)銷大,算下來(lái)性價(jià)比不高。我們就可以將這個(gè)函數(shù)聲明為內(nèi)聯(lián)函數(shù)。編譯器在編譯過(guò)程中遇到內(nèi)聯(lián)函數(shù)時(shí),像宏一樣,將內(nèi)聯(lián)函數(shù)直接在調(diào)用處展開(kāi)。這樣做的好處就是減少了函數(shù)調(diào)用開(kāi)銷,直接執(zhí)行內(nèi)聯(lián)函數(shù)展開(kāi)的代碼,不用再保存現(xiàn)場(chǎng)、恢復(fù)現(xiàn)場(chǎng)。
看到這里,可能就有人納悶了,內(nèi)聯(lián)函數(shù)既然跟宏的功能差不多,那為什么不直接定義一個(gè)宏,而去定義一個(gè)內(nèi)聯(lián)函數(shù)呢?
存在即合理,內(nèi)聯(lián)函數(shù)既然在 C 語(yǔ)言中廣泛應(yīng)用,自然有它存在的道理。相對(duì)于宏,內(nèi)聯(lián)函數(shù)有以下幾個(gè)優(yōu)勢(shì)。
前面也講過(guò),我們雖然可以通過(guò) inline 關(guān)鍵字,將一個(gè)函數(shù)聲明為內(nèi)聯(lián)函數(shù),但編譯器不一定會(huì)對(duì)這個(gè)內(nèi)聯(lián)函數(shù)展開(kāi)處理。編譯器也要進(jìn)行評(píng)估,權(quán)衡展開(kāi)和不展開(kāi)的利弊。
內(nèi)聯(lián)函數(shù)并不是完美無(wú)瑕,也有一些缺點(diǎn)。比如說(shuō),會(huì)增大程序的體積。如果在一個(gè)文件中多次調(diào)用內(nèi)聯(lián)函數(shù),多次展開(kāi),那整個(gè)程序的體積就會(huì)變大,在一定程度上,會(huì)造成 CPU 的取址效率降低,程序執(zhí)行效率降低。函數(shù)的作用之一就是提高代碼的復(fù)用性,我們將常用的一些代碼或代碼塊封裝成函數(shù),進(jìn)行模塊化編程,而內(nèi)聯(lián)函數(shù)往往是降低了函數(shù)的復(fù)用性。所以編譯器在對(duì)內(nèi)聯(lián)函數(shù)作展開(kāi)處理時(shí),除了檢測(cè)用戶定義的內(nèi)聯(lián)函數(shù)內(nèi)部是否有指針、循環(huán)、遞歸外,還會(huì)在函數(shù)執(zhí)行效率和函數(shù)調(diào)用開(kāi)銷之間進(jìn)行權(quán)衡。一般來(lái)講,判斷對(duì)一個(gè)內(nèi)聯(lián)函數(shù)到底展不展開(kāi),從程序員的角度,主要考慮以下幾個(gè)因素。
當(dāng)我們認(rèn)為一個(gè)函數(shù)體積小,而且被大量頻繁調(diào)用,應(yīng)該做內(nèi)聯(lián)展開(kāi)時(shí),就可以使用 static inline 關(guān)鍵字修飾它。但編譯器會(huì)不會(huì)作內(nèi)聯(lián)展開(kāi),編譯器也會(huì)有自己的權(quán)衡。如果你想告訴編譯器一定要展開(kāi),或者不作展開(kāi),就可以使用 noinline 或 always_inline 對(duì)函數(shù)作一個(gè)屬性聲明。
//inline.c
static inline
__attribute__((always_inline)) int func(int a)
{
return a+1;
}
static inline void print_num(int a)
{
printf("%d\n",a);
}
int main(void)
{
int i;
i=func(3);
print_num(10);
return 0;
}
在這個(gè)程序中,我們分別定義兩個(gè)內(nèi)聯(lián)函數(shù) func() 和 print_num(),然后使用 always_inline 對(duì) func() 函數(shù)進(jìn)行屬性聲明。接下來(lái),我們對(duì)生成的可執(zhí)行文件 a.out 作反匯編處理,其匯編代碼如下。
$ arm-linux-gnueabi-gcc -o a.out inline.c
$ arm-linux-gnueabi-objdump -D a.out
00010438 :
10438: e92d4800 push {fp, lr}
1043c: e28db004 add fp, sp, #4
10440: e24dd008 sub sp, sp, #8
10444: e50b0008 str r0, [fp, #-8]
10448: e51b1008 ldr r1, [fp, #-8]
1044c: e59f000c ldr r0, [pc, #12]
10450: ebffffa2 bl 102e0
10454: e1a00000 nop ; (mov r0, r0)
10458: e24bd004 sub sp, fp, #4
1045c: e8bd8800 pop {fp, pc}
10460: 0001050c andeq r0, r1, ip, lsl #10
00010464 :
10464: e92d4800 push {fp, lr}
10468: e28db004 add fp, sp, #4
1046c: e24dd008 sub sp, sp, #8
10470: e3a03003 mov r3, #3
10474: e50b3008 str r3, [fp, #-8]
10478: e51b3008 ldr r3, [fp, #-8]
1047c: e2833001 add r3, r3, #1
10480: e50b300c str r3, [fp, #-12]
10484: e3a0000a mov r0, #10
10488: ebffffea bl 10438
1048c: e3a03000 mov r3, #0
10490: e1a00003 mov r0, r3
10494: e24bd004 sub sp, fp, #4
10498: e8bd8800 pop {fp, pc}
通過(guò)反匯編代碼可以看到,因?yàn)槲覀儗?duì) func() 函數(shù)作了 always_inline 屬性聲明,所以編譯器在編譯過(guò)程中,對(duì)于 main()函數(shù)調(diào)用 func(),會(huì)直接在調(diào)用處展開(kāi)。
10470: e3a03003 mov r3, #3
10474: e50b3008 str r3, [fp, #-8]
10478: e51b3008 ldr r3, [fp, #-8]
1047c: e2833001 add r3, r3, #1
10480: e50b300c str r3, [fp, #-12]
而對(duì)于 print_num() 函數(shù),雖然我們對(duì)其作了內(nèi)聯(lián)聲明,但編譯器并沒(méi)有對(duì)其作內(nèi)聯(lián)展開(kāi),而是當(dāng)作一個(gè)普通函數(shù)對(duì)待。還有一個(gè)注意的細(xì)節(jié)是,當(dāng)編譯器對(duì)內(nèi)聯(lián)函數(shù)作展開(kāi)處理時(shí),會(huì)直接在調(diào)用處展開(kāi)內(nèi)聯(lián)函數(shù)的代碼,不再給 func() 函數(shù)本身生成單獨(dú)的匯編代碼。這是因?yàn)槠渌{(diào)用該函數(shù)的位置都作了內(nèi)聯(lián)展開(kāi),沒(méi)必要再去生成。在這個(gè)例子中,我們發(fā)現(xiàn)就沒(méi)有給 func() 函數(shù)本身生成單獨(dú)的匯編代碼,編譯器只給 print_num() 函數(shù)生成了獨(dú)立的匯編代碼。
在 Linux 內(nèi)核中,你會(huì)看到大量的內(nèi)聯(lián)函數(shù)定義在頭文件中,而且常常使用 static 修飾。
為什么 inline 函數(shù)經(jīng)常使用 static 修飾呢?這個(gè)問(wèn)題在網(wǎng)上也討論了很久,聽(tīng)起來(lái)各有道理,從 C 語(yǔ)言到 C++,甚至有人還拿出了 Linux 內(nèi)核作者 Linus 作者關(guān)于對(duì) static inline 的解釋:
"static inline" means "we have to have this function, if you use it, but don't inline it, then make a static version of it in this compilation unit". "extern inline" means "I actually have an extern for this function, but if you want to inline it, here's the inline-version".
我的理解是這樣的:內(nèi)聯(lián)函數(shù)為什么要定義在頭文件中呢?因?yàn)樗且粋€(gè)內(nèi)聯(lián)函數(shù),可以像宏一樣使用,任何想使用這個(gè)內(nèi)聯(lián)函數(shù)的源文件,不必親自再去定義一遍,直接包含這個(gè)頭文件,即可像宏一樣使用。那為什么還要用 static 修飾呢?因?yàn)槲覀兪褂?inline 定義的內(nèi)聯(lián)函數(shù),編譯器不一定會(huì)內(nèi)聯(lián)展開(kāi),那么當(dāng)多個(gè)文件都包含這個(gè)內(nèi)聯(lián)函數(shù)的定義時(shí),編譯時(shí)就有可能報(bào)重定義錯(cuò)誤。而使用 static 修飾,可以將這個(gè)函數(shù)的作用域局限在各自本地文件內(nèi),避免了重定義錯(cuò)誤。理解了這兩點(diǎn),就能夠看懂 Linux 內(nèi)核頭文件中定義的大部分內(nèi)聯(lián)函數(shù)了。至于其它的一些內(nèi)聯(lián)函數(shù)定義,基本上沒(méi)怎么遇到過(guò),就不再贅述了。
本教程根據(jù) C語(yǔ)言嵌入式Linux高級(jí)編程視頻教程 第05期 改編,電子版書籍可加入QQ群:475504428 下載,更多嵌入式視頻教程,可關(guān)注:
微信公眾號(hào):宅學(xué)部落(armlinuxfun)
51CTO學(xué)院-王利濤老師:http://edu.51cto.com/sd/d344f