溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

嵌入式C語言自我修養(yǎng) 10:內(nèi)聯(lián)函數(shù)探究

發(fā)布時間:2020-05-29 15:16:52 來源:網(wǎng)絡(luò) 閱讀:529 作者:宅學部落 欄目:系統(tǒng)運維

10.1 屬性聲明:noinline & always_inline

這一節(jié),接著講 attribute 屬性聲明,attribute可以說是 GNU C 最大的特色。我們接下來繼續(xù)講一下跟內(nèi)聯(lián)函數(shù)相關(guān)的兩個屬性:noinline 和 always_inline。這兩個屬性的用途是告訴編譯器:編譯時,對我們指定的函數(shù)內(nèi)聯(lián)展開或不展開。它們的使用方法如下。

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 修飾變量時,只是建議編譯器在給變量分配存儲空間時,將這個變量放到寄存器里,這樣,程序的運行效率會更高。那編譯器會不會放呢?編譯器就要根據(jù)寄存器資源緊不緊張,這個變量用得頻不頻繁來做權(quán)衡。

同樣,當一個函數(shù)使用 inline 關(guān)鍵字修飾,編譯器在編譯時一定會內(nèi)聯(lián)展開嗎?未必。編譯器也會根據(jù)實際情況,比如函數(shù)體大小、函數(shù)體內(nèi)是否有循環(huán)結(jié)構(gòu)、是否有指針、是否有遞歸、函數(shù)調(diào)用是否頻繁來做決定。比如 GCC 編譯器,一般是不會對內(nèi)聯(lián)函數(shù)展開的,只有當編譯優(yōu)化選項開到 -O2 以上,才會考慮是否內(nèi)聯(lián)展開。當我們使用 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ǔ)知識。

10.2 什么是內(nèi)聯(lián)函數(shù)

函數(shù)調(diào)用開銷

說起內(nèi)聯(lián)函數(shù),又不得不說函數(shù)調(diào)用開銷。一個函數(shù)在執(zhí)行過程中,如果需要調(diào)用其它函數(shù),一般會執(zhí)行下面這個過程。

  • 保存當前函數(shù)現(xiàn)場
  • 跳到調(diào)用函數(shù)執(zhí)行
  • 恢復(fù)當前函數(shù)現(xiàn)場
  • 繼續(xù)執(zhí)行當前函數(shù)

比如一個 ARM 程序,在一個函數(shù) f1() 中,我們對一些數(shù)據(jù)進行處理,運算結(jié)果暫時保存在 R0 寄存器中。接著要調(diào)用另外一個函數(shù) f2(),調(diào)用結(jié)束后,接著返回到 f1() 函數(shù)中繼續(xù)處理數(shù)據(jù)。如果我們在 f2() 函數(shù)中使用到 R0 這個寄存器(用于保存函數(shù)的返回值),此時就會改變 R0 寄存器中的值,那么就篡改了 f1() 函數(shù)中的暫存運算結(jié)果。當我們返回到 f1() 函數(shù)中繼續(xù)進行運算時,結(jié)果肯定不正確。

那怎么辦呢?很簡單,在跳到 f2() 執(zhí)行之前,先把 R0 寄存器的值保存到堆棧中,f() 函數(shù)執(zhí)行結(jié)束后,再將堆棧中的值恢復(fù)到 R0 寄存器中,這樣 f1() 函數(shù)就可以接著繼續(xù)執(zhí)行了,就跟什么事情都沒發(fā)生過一樣。

這種方法證明是 OK 的,現(xiàn)代計算機系統(tǒng),無論是什么架構(gòu)和指令集,都是采用這種方法。雖然麻煩了點,但至少能解決問題,無非就是多花點代價,需要不斷地保存現(xiàn)場、恢復(fù)現(xiàn)場,這就是函數(shù)調(diào)用帶來的開銷。

內(nèi)聯(lián)函數(shù)的好處

對于一般的函數(shù)調(diào)用,這種方法是沒有問題的。但對于一些極端情況,比如說一個函數(shù)很小,函數(shù)體內(nèi)只有一行代碼,而且被大量頻繁的調(diào)用。如果每次調(diào)用,都不斷地保存現(xiàn)場,執(zhí)行時卻發(fā)現(xiàn)函數(shù)只有一行代碼,又要恢復(fù)現(xiàn)場,往往造成函數(shù)開銷比較大,性價比不高。這就跟你去五星級飯店訂個餐位吃飯一樣,VIP 包間、刀叉餐具、空調(diào)、服務(wù)人員都準備好了,你到了之后只要了一碗面條,吃完之后抹嘴走人,而且一天三頓你都這么干,你說服務(wù)員煩不煩?

函數(shù)調(diào)用也是如此。有些函數(shù)很小,而且調(diào)用頻繁,調(diào)用開銷大,算下來性價比不高。我們就可以將這個函數(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)場。

10.3 內(nèi)聯(lián)函數(shù)與宏

看到這里,可能就有人納悶了,內(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)勢。

  • 參數(shù)類型檢查。內(nèi)聯(lián)函數(shù)雖然具有宏的展開特性,但其本質(zhì)仍是函數(shù),編譯過程中,編譯器仍可以對其進行參數(shù)檢查,而宏就不具備這個功能。
  • 便于調(diào)試。函數(shù)支持的調(diào)試功能有斷點、單步……,內(nèi)聯(lián)函數(shù)也同樣可以。
  • 返回值。內(nèi)聯(lián)函數(shù)有返回值,返回一個結(jié)果給調(diào)用者。這個優(yōu)勢是相對于 ANSI C 說的。不過現(xiàn)在宏也可以有返回值和類型了,比如前面我們使用語句表達式定義的宏。
  • 接口封裝。有些內(nèi)聯(lián)函數(shù)可以用來封裝一個接口,而宏不具備這個特性。

10.4 編譯器對內(nèi)聯(lián)函數(shù)的處理

前面也講過,我們雖然可以通過 inline 關(guān)鍵字,將一個函數(shù)聲明為內(nèi)聯(lián)函數(shù),但編譯器不一定會對這個內(nèi)聯(lián)函數(shù)展開處理。編譯器也要進行評估,權(quán)衡展開和不展開的利弊。

內(nèi)聯(lián)函數(shù)并不是完美無瑕,也有一些缺點。比如說,會增大程序的體積。如果在一個文件中多次調(diào)用內(nèi)聯(lián)函數(shù),多次展開,那整個程序的體積就會變大,在一定程度上,會造成 CPU 的取址效率降低,程序執(zhí)行效率降低。函數(shù)的作用之一就是提高代碼的復(fù)用性,我們將常用的一些代碼或代碼塊封裝成函數(shù),進行模塊化編程,而內(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)用開銷之間進行權(quán)衡。一般來講,判斷對一個內(nèi)聯(lián)函數(shù)到底展不展開,從程序員的角度,主要考慮以下幾個因素。

  • 函數(shù)體積小且調(diào)用頻繁
  • 函數(shù)體內(nèi)無遞歸、循環(huán)等語句
  • 函數(shù)本身作為一個函數(shù)指針賦值在別處被引用
  • 函數(shù)和caller是否在同一個文件內(nèi)

當我們認為一個函數(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ù)進行屬性聲明。接下來,我們對生成的可執(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}

通過反匯編代碼可以看到,因為我們對 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)展開,而是當作一個普通函數(shù)對待。還有一個注意的細節(jié)是,當編譯器對內(nèi)聯(lián)函數(shù)作展開處理時,會直接在調(diào)用處展開內(nèi)聯(lián)函數(shù)的代碼,不再給 func() 函數(shù)本身生成單獨的匯編代碼。這是因為其它調(diào)用該函數(shù)的位置都作了內(nèi)聯(lián)展開,沒必要再去生成。在這個例子中,我們發(fā)現(xiàn)就沒有給 func() 函數(shù)本身生成單獨的匯編代碼,編譯器只給 print_num() 函數(shù)生成了獨立的匯編代碼。

10.5 思考:內(nèi)聯(lián)函數(shù)為什么常使用 static 修飾?

在 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ù)為什么要定義在頭文件中呢?因為它是一個內(nèi)聯(lián)函數(shù),可以像宏一樣使用,任何想使用這個內(nèi)聯(lián)函數(shù)的源文件,不必親自再去定義一遍,直接包含這個頭文件,即可像宏一樣使用。那為什么還要用 static 修飾呢?因為我們使用 inline 定義的內(nèi)聯(lián)函數(shù),編譯器不一定會內(nèi)聯(lián)展開,那么當多個文件都包含這個內(nèi)聯(lián)函數(shù)的定義時,編譯時就有可能報重定義錯誤。而使用 static 修飾,可以將這個函數(shù)的作用域局限在各自本地文件內(nèi),避免了重定義錯誤。理解了這兩點,就能夠看懂 Linux 內(nèi)核頭文件中定義的大部分內(nèi)聯(lián)函數(shù)了。至于其它的一些內(nèi)聯(lián)函數(shù)定義,基本上沒怎么遇到過,就不再贅述了。

本教程根據(jù) C語言嵌入式Linux高級編程視頻教程 第05期 改編,電子版書籍可加入QQ群:475504428 下載,更多嵌入式視頻教程,可關(guān)注:
微信公眾號:宅學部落(armlinuxfun)
51CTO學院-王利濤老師:http://edu.51cto.com/sd/d344f

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI