打開 Linux 內核原始碼,會發現內核在定義C語言函數時,有很多都帶有 「inline」關鍵字,請看下圖,那麼這個關鍵字有什麼作用呢?
inline 關鍵字的作用
在C語言程序開發中,inline 一般用於定義函數,inline 函數也被稱作「內聯函數」,C99 和 GNU C 均支持內聯函數。那麼在C語言中,內聯函數和普通函數有什麼不同呢?其實,從 inline 這個名字就應該能看出一點它的性質了——內聯函數會在它被調用的位置上展開,這一點表現的和 define 宏定義是非常相似的。
將被調用的函數代碼展開,作業系統就無需再在為被調用函數做申請棧幀和回收棧幀的工作,而且,由於編譯器會把被調用的函數代碼和函數本身放在一起優化,所以也有進一步優化C語言代碼,提升效率的可能。
每發生一次函數調用,作業系統就要在程序的棧空間申請一塊內存區域(棧幀),供被調用函數使用,被調用函數執行完畢後,作業系統還要回收這些內存。
不過,天下沒有免費的午餐,C語言程序要實現內聯函數的上述特性是要付出一定的代價的。普通函數只需要編譯出一份,就可以被所有其他函數調用,而內聯函數沒有嚴格意義上的「調用」,它只是將自身的代碼展開到被調用處的,這麼做無疑會使整個C語言代碼變長,也就意味著佔用更多的內存空間,以及更多的指令緩存。
顯然,如果濫用內聯函數,cpu 的指令緩存肯定是不夠用的,這會導致 cpu 緩存命中率降低,反而可能會降低整個C語言程序的效率。因此,建議把那些對時間要求比較高,且C語言代碼長度比較短的函數定義為內聯函數。如果在C語言程序開發中的某個函數比較大,又會被反覆調用,並且沒有特別的時間限制,是不適合把它做成內聯函數的。
在 Linux 內核中,內聯函數常常使用 static 修飾,例如:
staticinlinevoid set_value(unsignedint val){ ...}需要注意的是,內聯函數必須在使用之前就定義好,否則編譯器沒法把這個函數展開。Linux 內核中經常像下面這樣,將內聯函數放在調用它的函數前面,請看C語言代碼:
staticinlinevoid set_value(unsignedint val){ ...}int test_inline(){ set_value(3); ...}
所以,Linux 內核常常把內聯函數定義在頭文件裡,這樣在其他C語言代碼文件開頭包含頭文件時,能確保內聯函數在文件的最開始,無需再寫額外的聲明語句。
這也解釋了為什麼 Linux 內核為何常常使用 static 修飾內聯函數,因為可以避免函數的重複定義。
前文提到內聯函數的表現有些像 define 宏定義,但是為了類型安全和易讀性,應優先使用內聯函數而不是複雜的宏。下面通過實例進一步分析 inline 內聯函數的特性。
inline內聯函數的「展開代碼」是什麼意思?
使用過 define 寫 C語言代碼的朋友應該都知道,編譯器在編譯 C語言代碼時,會將 define 定義的宏展開,而不是像普通函數那樣使用 call 指令調用,例如下面這段C語言代碼:
#include <stdio.h>#define d_add(a, b) ((a)+(b))int f_add(int a, int b){ return a+b;}int main(){ int a = d_add(1, 2); int b = f_add(1, 2); return0;}
使用 gcc -E 編譯這段C語言代碼,能夠得到預處理後的代碼如下,顯然 define 定義的宏被展開了,請看:
使用 gcc -g 命令編譯C語言代碼,得到可執行文件,然後調用 objdump 命令查看彙編代碼,得到如下結果:
# gcc -g t1.c # objdump -dS a.out
從 f_add() 函數的彙編代碼也可以看出,程序首先將 2 個參數賦值給寄存器,然後使用 call 指令調用 f_add() 函數。而宏定義 d_add() 就簡單了,只有一行彙編代碼,這種情況下,使用 define 宏定義顯然效率更高。不過,宏定義沒有參數的類型檢查,使用起來不太安全,好在C語言還有 inline 函數,下面再定義一個 inline 函數,請看C語言代碼如下:
staticinlineint i_add(int a, int b){ return a+b;}
在 main() 函數中使用 gcc -E 命令查看添加 inline 函數後的C語言代碼預處理結果,如下:
可以看出,在預處理階段,inline 函數並沒有像 define 宏那樣展開。現在使用 gcc -g 命令編譯得到可執行文件,然後使用 objdump 查看彙編代碼,如下:
從彙編代碼可以看出,inline 函數似乎並沒有起到作用,i_add() 函數和 f_add() 函數的表現並沒有什麼不同,繼續往上查看,發現編譯器也將 i_add() 函數的彙編代碼生成了,這無疑是將 i_add() 函數當作普通函數使用了:
staticinlineint i_add(int a, int b){ 400501: 55 push %rbp 400502: 4889 e5 mov %rsp,%rbp 400505: 897d fc mov %edi,-0x4(%rbp) 400508: 8975 f8 mov %esi,-0x8(%rbp) return a+b; 40050b: 8b 45 f8 mov -0x8(%rbp),%eax 40050e: 8b 55 fc mov -0x4(%rbp),%edx 400511: 01 d0 add %edx,%eax} 400513: 5d pop %rbp 400514: c3 retq怎麼回事?不是說 inline 函數的表現和 define 宏相似,會將函數代碼展開嗎?其實,inline 只是建議編譯器這麼做,編譯器究竟會不會這麼做就不一定了。這與編譯器的優化級別相關,請看下圖:
gcc 的 -O 選項可以指定優化級別,我們上面編譯程序時沒有使用 -O 選項,因此編譯器執行的是默認的 -O0,也即無優化編譯。那能否在 -O0 優化級別也使用 inline 函數的特性呢?當然是可以的,只需要在定義 inline 函數時,添加 __attribute__((always_inline)) 即可,例如:
static __attribute__((always_inline)) inlineint i_add(int a, int b){ return a+b;}
現在再來編譯C語言程序並查看彙編代碼,得到如下結果:
這種情況下,編譯器並沒有為 i_add() 函數生成響應的彙編代碼。雖然 inline 函數在預處理階段沒有像 define 宏定義那樣展開,但是在生成彙編代碼階段展開了,而且參與了調用它的代碼部分的優化,這顯然會讓整個C語言程序的效率提高。
inline 函數雖然表現上很像 define 宏定義,但是卻並不能完全取代 define 宏定義,這一點在我之後的文章裡會討論,敬請關注。
小結
在 C語言程序開發中,建議把那些對時間要求比較高,且C語言代碼長度比較短的函數定義為 inline 函數,這麼做常常可以提升程序的效率。在默認的 -O0 編譯優化項不能確保 inline 一定起作用,但是可以添加添加 __attribute__((always_inline))強制編譯器對 inline 函數做相應的處理。因為 inline 函數會將自己展開,所以編譯器通常不會再為 inline 生成彙編代碼,不過,如果是通過函數指針的形式調用 inline 函數,編譯器為了獲得 inline 函數的地址,仍然會為其生成彙編代碼的。