這一節(jié),接著講 attribute 屬性聲明,attribute可以說是 GNU C 最大的特色。我們接下來繼續(xù)講一下跟內(nèi)聯(lián)函數(shù)相關(guān)的兩個屬性:noinline 和 always_inline。這兩個屬性的用途是告訴編譯器:編譯時,對我們指定的函數(shù)內(nèi)聯(lián)展開或不展開。它們的使用方法如下。
成都創(chuàng)新互聯(lián)公司2013年開創(chuàng)至今,先為靈璧等服務(wù)建站,靈璧等地企業(yè),進(jìn)行企業(yè)商務(wù)咨詢服務(wù)。為靈璧企業(yè)網(wǎng)站制作PC+手機(jī)+微官網(wǎng)三網(wǎng)同步一站式服務(wù)解決您的所有建站問題。
static inline __attribute__((noinline)) int func();
static inline __attribute__((always_inline)) int func();
內(nèi)聯(lián)函數(shù)使用 inline 聲明即可,有時候還會用 static 和 extern 修飾。使用 inline 聲明一個內(nèi)聯(lián)函數(shù),和使用關(guān)鍵字 register 聲明一個變量一樣,只是建議編譯器在編譯時內(nèi)聯(lián)展開。使用關(guān)鍵字 register 修飾變量時,只是建議編譯器在給變量分配存儲空間時,將這個變量放到寄存器里,這樣,程序的運(yùn)行效率會更高。那編譯器會不會放呢?編譯器就要根據(jù)寄存器資源緊不緊張,這個變量用得頻不頻繁來做權(quán)衡。
同樣,當(dāng)一個函數(shù)使用 inline 關(guān)鍵字修飾,編譯器在編譯時一定會內(nèi)聯(lián)展開嗎?未必。編譯器也會根據(jù)實(shí)際情況,比如函數(shù)體大小、函數(shù)體內(nèi)是否有循環(huán)結(jié)構(gòu)、是否有指針、是否有遞歸、函數(shù)調(diào)用是否頻繁來做決定。比如 GCC 編譯器,一般是不會對內(nèi)聯(lián)函數(shù)展開的,只有當(dāng)編譯優(yōu)化選項(xiàng)開到 -O2 以上,才會考慮是否內(nèi)聯(lián)展開。當(dāng)我們使用 noinline 和 always_inline 對一個內(nèi)聯(lián)函數(shù)作了屬性聲明后,編譯器的編譯行為就變得確定了。使用 noinline 聲明,就是告訴編譯器,不要展開;使用 always_inline 屬性聲明,就是告訴編譯器,要內(nèi)聯(lián)展開。
什么是內(nèi)聯(lián)展開呢?我們不得不說一下內(nèi)聯(lián)函數(shù)的基礎(chǔ)知識。
說起內(nèi)聯(lián)函數(shù),又不得不說函數(shù)調(diào)用開銷。一個函數(shù)在執(zhí)行過程中,如果需要調(diào)用其它函數(shù),一般會執(zhí)行下面這個過程。
比如一個 ARM 程序,在一個函數(shù) f1() 中,我們對一些數(shù)據(jù)進(jìn)行處理,運(yùn)算結(jié)果暫時保存在 R0 寄存器中。接著要調(diào)用另外一個函數(shù) f2(),調(diào)用結(jié)束后,接著返回到 f1() 函數(shù)中繼續(xù)處理數(shù)據(jù)。如果我們在 f2() 函數(shù)中使用到 R0 這個寄存器(用于保存函數(shù)的返回值),此時就會改變 R0 寄存器中的值,那么就篡改了 f1() 函數(shù)中的暫存運(yùn)算結(jié)果。當(dāng)我們返回到 f1() 函數(shù)中繼續(xù)進(jìn)行運(yùn)算時,結(jié)果肯定不正確。
那怎么辦呢?很簡單,在跳到 f2() 執(zhí)行之前,先把 R0 寄存器的值保存到堆棧中,f() 函數(shù)執(zhí)行結(jié)束后,再將堆棧中的值恢復(fù)到 R0 寄存器中,這樣 f1() 函數(shù)就可以接著繼續(xù)執(zhí)行了,就跟什么事情都沒發(fā)生過一樣。
這種方法證明是 OK 的,現(xiàn)代計(jì)算機(jī)系統(tǒng),無論是什么架構(gòu)和指令集,都是采用這種方法。雖然麻煩了點(diǎn),但至少能解決問題,無非就是多花點(diǎn)代價(jià),需要不斷地保存現(xiàn)場、恢復(fù)現(xiàn)場,這就是函數(shù)調(diào)用帶來的開銷。
對于一般的函數(shù)調(diào)用,這種方法是沒有問題的。但對于一些極端情況,比如說一個函數(shù)很小,函數(shù)體內(nèi)只有一行代碼,而且被大量頻繁的調(diào)用。如果每次調(diào)用,都不斷地保存現(xiàn)場,執(zhí)行時卻發(fā)現(xiàn)函數(shù)只有一行代碼,又要恢復(fù)現(xiàn)場,往往造成函數(shù)開銷比較大,性價(jià)比不高。這就跟你去五星級飯店訂個餐位吃飯一樣,VIP 包間、刀叉餐具、空調(diào)、服務(wù)人員都準(zhǔn)備好了,你到了之后只要了一碗面條,吃完之后抹嘴走人,而且一天三頓你都這么干,你說服務(wù)員煩不煩?
函數(shù)調(diào)用也是如此。有些函數(shù)很小,而且調(diào)用頻繁,調(diào)用開銷大,算下來性價(jià)比不高。我們就可以將這個函數(shù)聲明為內(nèi)聯(lián)函數(shù)。編譯器在編譯過程中遇到內(nèi)聯(lián)函數(shù)時,像宏一樣,將內(nèi)聯(lián)函數(shù)直接在調(diào)用處展開。這樣做的好處就是減少了函數(shù)調(diào)用開銷,直接執(zhí)行內(nèi)聯(lián)函數(shù)展開的代碼,不用再保存現(xiàn)場、恢復(fù)現(xiàn)場。
看到這里,可能就有人納悶了,內(nèi)聯(lián)函數(shù)既然跟宏的功能差不多,那為什么不直接定義一個宏,而去定義一個內(nèi)聯(lián)函數(shù)呢?
存在即合理,內(nèi)聯(lián)函數(shù)既然在 C 語言中廣泛應(yīng)用,自然有它存在的道理。相對于宏,內(nèi)聯(lián)函數(shù)有以下幾個優(yōu)勢。
前面也講過,我們雖然可以通過 inline 關(guān)鍵字,將一個函數(shù)聲明為內(nèi)聯(lián)函數(shù),但編譯器不一定會對這個內(nèi)聯(lián)函數(shù)展開處理。編譯器也要進(jìn)行評估,權(quán)衡展開和不展開的利弊。
內(nèi)聯(lián)函數(shù)并不是完美無瑕,也有一些缺點(diǎn)。比如說,會增大程序的體積。如果在一個文件中多次調(diào)用內(nèi)聯(lián)函數(shù),多次展開,那整個程序的體積就會變大,在一定程度上,會造成 CPU 的取址效率降低,程序執(zhí)行效率降低。函數(shù)的作用之一就是提高代碼的復(fù)用性,我們將常用的一些代碼或代碼塊封裝成函數(shù),進(jìn)行模塊化編程,而內(nèi)聯(lián)函數(shù)往往是降低了函數(shù)的復(fù)用性。所以編譯器在對內(nèi)聯(lián)函數(shù)作展開處理時,除了檢測用戶定義的內(nèi)聯(lián)函數(shù)內(nèi)部是否有指針、循環(huán)、遞歸外,還會在函數(shù)執(zhí)行效率和函數(shù)調(diào)用開銷之間進(jìn)行權(quán)衡。一般來講,判斷對一個內(nèi)聯(lián)函數(shù)到底展不展開,從程序員的角度,主要考慮以下幾個因素。
當(dāng)我們認(rèn)為一個函數(shù)體積小,而且被大量頻繁調(diào)用,應(yīng)該做內(nèi)聯(lián)展開時,就可以使用 static inline 關(guān)鍵字修飾它。但編譯器會不會作內(nèi)聯(lián)展開,編譯器也會有自己的權(quán)衡。如果你想告訴編譯器一定要展開,或者不作展開,就可以使用 noinline 或 always_inline 對函數(shù)作一個屬性聲明。
//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;
}
在這個程序中,我們分別定義兩個內(nèi)聯(lián)函數(shù) func() 和 print_num(),然后使用 always_inline 對 func() 函數(shù)進(jìn)行屬性聲明。接下來,我們對生成的可執(zhí)行文件 a.out 作反匯編處理,其匯編代碼如下。
$ arm-linux-gnueabi-gcc -o a.out inline.c
$ arm-linux-gnueabi-objdump -D a.out
00010438 <print_num>:
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 <printf@plt>
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 <main>:
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 <print_num>
1048c: e3a03000 mov r3, #0
10490: e1a00003 mov r0, r3
10494: e24bd004 sub sp, fp, #4
10498: e8bd8800 pop {fp, pc}
通過反匯編代碼可以看到,因?yàn)槲覀儗?func() 函數(shù)作了 always_inline 屬性聲明,所以編譯器在編譯過程中,對于 main()函數(shù)調(diào)用 func(),會直接在調(diào)用處展開。
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]
而對于 print_num() 函數(shù),雖然我們對其作了內(nèi)聯(lián)聲明,但編譯器并沒有對其作內(nèi)聯(lián)展開,而是當(dāng)作一個普通函數(shù)對待。還有一個注意的細(xì)節(jié)是,當(dāng)編譯器對內(nèi)聯(lián)函數(shù)作展開處理時,會直接在調(diào)用處展開內(nèi)聯(lián)函數(shù)的代碼,不再給 func() 函數(shù)本身生成單獨(dú)的匯編代碼。這是因?yàn)槠渌{(diào)用該函數(shù)的位置都作了內(nèi)聯(lián)展開,沒必要再去生成。在這個例子中,我們發(fā)現(xiàn)就沒有給 func() 函數(shù)本身生成單獨(dú)的匯編代碼,編譯器只給 print_num() 函數(shù)生成了獨(dú)立的匯編代碼。
在 Linux 內(nèi)核中,你會看到大量的內(nèi)聯(lián)函數(shù)定義在頭文件中,而且常常使用 static 修飾。
為什么 inline 函數(shù)經(jīng)常使用 static 修飾呢?這個問題在網(wǎng)上也討論了很久,聽起來各有道理,從 C 語言到 C++,甚至有人還拿出了 Linux 內(nèi)核作者 Linus 作者關(guān)于對 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)樗且粋€內(nèi)聯(lián)函數(shù),可以像宏一樣使用,任何想使用這個內(nèi)聯(lián)函數(shù)的源文件,不必親自再去定義一遍,直接包含這個頭文件,即可像宏一樣使用。那為什么還要用 static 修飾呢?因?yàn)槲覀兪褂?inline 定義的內(nèi)聯(lián)函數(shù),編譯器不一定會內(nèi)聯(lián)展開,那么當(dāng)多個文件都包含這個內(nèi)聯(lián)函數(shù)的定義時,編譯時就有可能報(bào)重定義錯誤。而使用 static 修飾,可以將這個函數(shù)的作用域局限在各自本地文件內(nèi),避免了重定義錯誤。理解了這兩點(diǎn),就能夠看懂 Linux 內(nèi)核頭文件中定義的大部分內(nèi)聯(lián)函數(shù)了。至于其它的一些內(nèi)聯(lián)函數(shù)定義,基本上沒怎么遇到過,就不再贅述了。
本教程根據(jù) C語言嵌入式Linux高級編程視頻教程 第05期 改編,電子版書籍可加入QQ群:475504428 下載,更多嵌入式視頻教程,可關(guān)注:
微信公眾號:宅學(xué)部落(armlinuxfun)
51CTO學(xué)院-王利濤老師:http://edu.51cto.com/sd/d344f
文章名稱:嵌入式C語言自我修養(yǎng)10:內(nèi)聯(lián)函數(shù)探究
當(dāng)前鏈接:http://www.rwnh.cn/article4/gspjie.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供服務(wù)器托管、品牌網(wǎng)站制作、企業(yè)建站、品牌網(wǎng)站建設(shè)、定制開發(fā)、外貿(mào)建站
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請盡快告知,我們將會在第一時間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時需注明來源: 創(chuàng)新互聯(lián)