開啟 Linux 核心原始碼,會發現核心在定義C語言函式時,有很多都帶有 “inline”關鍵字,請看下圖,那麼這個關鍵字有什麼作用呢?
在C語言程式開發中,inline 一般用於定義函式,inline 函式也被稱作“行內函數”,C99 和 GNU C 均支援行內函數。那麼在C語言中,行內函數和普通函式有什麼不同呢?其實,從 inline 這個名字就應該能看出一點它的性質了——行內函數會在它被呼叫的位置上展開,這一點表現的和 define 宏定義是非常相似的。
將被呼叫的函式程式碼展開,作業系統就無需再在為被呼叫函式做申請棧幀和回收棧幀的工作,而且,由於編譯器會把被呼叫的函式程式碼和函式本身放在一起最佳化,所以也有進一步最佳化C語言程式碼,提升效率的可能。
每發生一次函式呼叫,作業系統就要在程式的棧空間申請一塊記憶體區域(棧幀),供被呼叫函式使用,被呼叫函式執行完畢後,作業系統還要回收這些記憶體。
不過,天下沒有免費的午餐,C語言程式要實現行內函數的上述特性是要付出一定的代價的。普通函式只需要編譯出一份,就可以被所有其他函式呼叫,而行內函數沒有嚴格意義上的“呼叫”,它只是將自身的程式碼展開到被呼叫處的,這麼做無疑會使整個C語言程式碼變長,也就意味著佔用更多的記憶體空間,以及更多的指令快取。
顯然,如果濫用行內函數,cpu 的指令快取肯定是不夠用的,這會導致 cpu 快取命中率降低,反而可能會降低整個C語言程式的效率。因此,建議把那些對時間要求比較高,且C語言程式碼長度比較短的函式定義為行內函數。如果在C語言程式開發中的某個函式比較大,又會被反覆呼叫,並且沒有特別的時間限制,是不適合把它做成行內函數的。
在 Linux 核心中,行內函數常常使用 static 修飾,例如:
需要注意的是,行內函數必須在使用之前就定義好,否則編譯器沒法把這個函式展開。Linux 核心中經常像下面這樣,將行內函數放在呼叫它的函式前面,請看C語言程式碼:
所以,Linux 核心常常把行內函數定義在標頭檔案裡,這樣在其他C語言程式碼檔案開頭包含標頭檔案時,能確保行內函數在檔案的最開始,無需再寫額外的宣告語句。
這也解釋了為什麼 Linux 核心為何常常使用 static 修飾行內函數,因為可以避免函式的重複定義。
前文提到行內函數的表現有些像 define 宏定義,但是為了型別安全和易讀性,應優先使用行內函數而不是複雜的宏。下面透過例項進一步分析 inline 行內函數的特性。
使用過 define 寫 C語言程式碼的朋友應該都知道,編譯器在編譯 C語言程式碼時,會將 define 定義的宏展開,而不是像普通函式那樣使用 call 指令呼叫,例如下面這段C語言程式碼:
使用 gcc -E 編譯這段C語言程式碼,能夠得到預處理後的程式碼如下,顯然 define 定義的宏被展開了,請看:
使用 gcc -g 命令編譯C語言程式碼,得到可執行檔案,然後呼叫 objdump 命令檢視彙編程式碼,得到如下結果:
從 f_add() 函式的彙編程式碼也可以看出,程式首先將 2 個引數賦值給暫存器,然後使用 call 指令呼叫 f_add() 函式。而宏定義 d_add() 就簡單了,只有一行彙編程式碼,這種情況下,使用 define 宏定義顯然效率更高。不過,宏定義沒有引數的型別檢查,使用起來不太安全,好在C語言還有 inline 函式,下面再定義一個 inline 函式,請看C語言程式碼如下:
在 main() 函式中使用 gcc -E 命令檢視新增 inline 函式後的C語言程式碼預處理結果,如下:
可以看出,在預處理階段,inline 函式並沒有像 define 宏那樣展開。現在使用 gcc -g 命令編譯得到可執行檔案,然後使用 objdump 檢視彙編程式碼,如下:
從彙編程式碼可以看出,inline 函式似乎並沒有起到作用,i_add() 函式和 f_add() 函式的表現並沒有什麼不同,繼續往上檢視,發現編譯器也將 i_add() 函式的彙編程式碼生成了,這無疑是將 i_add() 函式當作普通函式使用了:
怎麼回事?不是說 inline 函式的表現和 define 宏相似,會將函式程式碼展開嗎?其實,inline 只是建議編譯器這麼做,編譯器究竟會不會這麼做就不一定了。這與編譯器的最佳化級別相關,請看下圖:
gcc 的 -O 選項可以指定最佳化級別,我們上面編譯程式時沒有使用 -O 選項,因此編譯器執行的是預設的 -O0,也即無最佳化編譯。那能否在 -O0 最佳化級別也使用 inline 函式的特性呢?當然是可以的,只需要在定義 inline 函式時,新增 __attribute__((always_inline)) 即可,例如:
現在再來編譯C語言程式並檢視彙編程式碼,得到如下結果:
這種情況下,編譯器並沒有為 i_add() 函式生成響應的彙編程式碼。雖然 inline 函式在預處理階段沒有像 define 宏定義那樣展開,但是在生成彙編程式碼階段展開了,而且參與了呼叫它的程式碼部分的最佳化,這顯然會讓整個C語言程式的效率提高。
在 C語言程式開發中,建議把那些對時間要求比較高,且C語言程式碼長度比較短的函式定義為 inline 函式,這麼做常常可以提升程式的效率。在預設的 -O0 編譯最佳化項不能確保 inline 一定起作用,但是可以新增新增 __attribute__((always_inline))強制編譯器對 inline 函式做相應的處理。因為 inline 函式會將自己展開,所以編譯器通常不會再為 inline 生成彙編程式碼,不過,如果是透過函式指標的形式呼叫 inline 函式,編譯器為了獲得 inline 函式的地址,仍然會為其生成彙編程式碼的。
開啟 Linux 核心原始碼,會發現核心在定義C語言函式時,有很多都帶有 “inline”關鍵字,請看下圖,那麼這個關鍵字有什麼作用呢?
inline 關鍵字的作用在C語言程式開發中,inline 一般用於定義函式,inline 函式也被稱作“行內函數”,C99 和 GNU C 均支援行內函數。那麼在C語言中,行內函數和普通函式有什麼不同呢?其實,從 inline 這個名字就應該能看出一點它的性質了——行內函數會在它被呼叫的位置上展開,這一點表現的和 define 宏定義是非常相似的。
將被呼叫的函式程式碼展開,作業系統就無需再在為被呼叫函式做申請棧幀和回收棧幀的工作,而且,由於編譯器會把被呼叫的函式程式碼和函式本身放在一起最佳化,所以也有進一步最佳化C語言程式碼,提升效率的可能。
每發生一次函式呼叫,作業系統就要在程式的棧空間申請一塊記憶體區域(棧幀),供被呼叫函式使用,被呼叫函式執行完畢後,作業系統還要回收這些記憶體。
不過,天下沒有免費的午餐,C語言程式要實現行內函數的上述特性是要付出一定的代價的。普通函式只需要編譯出一份,就可以被所有其他函式呼叫,而行內函數沒有嚴格意義上的“呼叫”,它只是將自身的程式碼展開到被呼叫處的,這麼做無疑會使整個C語言程式碼變長,也就意味著佔用更多的記憶體空間,以及更多的指令快取。
顯然,如果濫用行內函數,cpu 的指令快取肯定是不夠用的,這會導致 cpu 快取命中率降低,反而可能會降低整個C語言程式的效率。因此,建議把那些對時間要求比較高,且C語言程式碼長度比較短的函式定義為行內函數。如果在C語言程式開發中的某個函式比較大,又會被反覆呼叫,並且沒有特別的時間限制,是不適合把它做成行內函數的。
在 Linux 核心中,行內函數常常使用 static 修飾,例如:
需要注意的是,行內函數必須在使用之前就定義好,否則編譯器沒法把這個函式展開。Linux 核心中經常像下面這樣,將行內函數放在呼叫它的函式前面,請看C語言程式碼:
所以,Linux 核心常常把行內函數定義在標頭檔案裡,這樣在其他C語言程式碼檔案開頭包含標頭檔案時,能確保行內函數在檔案的最開始,無需再寫額外的宣告語句。
這也解釋了為什麼 Linux 核心為何常常使用 static 修飾行內函數,因為可以避免函式的重複定義。
前文提到行內函數的表現有些像 define 宏定義,但是為了型別安全和易讀性,應優先使用行內函數而不是複雜的宏。下面透過例項進一步分析 inline 行內函數的特性。
inline行內函數的“展開程式碼”是什麼意思?使用過 define 寫 C語言程式碼的朋友應該都知道,編譯器在編譯 C語言程式碼時,會將 define 定義的宏展開,而不是像普通函式那樣使用 call 指令呼叫,例如下面這段C語言程式碼:
使用 gcc -E 編譯這段C語言程式碼,能夠得到預處理後的程式碼如下,顯然 define 定義的宏被展開了,請看:
使用 gcc -g 命令編譯C語言程式碼,得到可執行檔案,然後呼叫 objdump 命令檢視彙編程式碼,得到如下結果:
從 f_add() 函式的彙編程式碼也可以看出,程式首先將 2 個引數賦值給暫存器,然後使用 call 指令呼叫 f_add() 函式。而宏定義 d_add() 就簡單了,只有一行彙編程式碼,這種情況下,使用 define 宏定義顯然效率更高。不過,宏定義沒有引數的型別檢查,使用起來不太安全,好在C語言還有 inline 函式,下面再定義一個 inline 函式,請看C語言程式碼如下:
在 main() 函式中使用 gcc -E 命令檢視新增 inline 函式後的C語言程式碼預處理結果,如下:
可以看出,在預處理階段,inline 函式並沒有像 define 宏那樣展開。現在使用 gcc -g 命令編譯得到可執行檔案,然後使用 objdump 檢視彙編程式碼,如下:
從彙編程式碼可以看出,inline 函式似乎並沒有起到作用,i_add() 函式和 f_add() 函式的表現並沒有什麼不同,繼續往上檢視,發現編譯器也將 i_add() 函式的彙編程式碼生成了,這無疑是將 i_add() 函式當作普通函式使用了:
怎麼回事?不是說 inline 函式的表現和 define 宏相似,會將函式程式碼展開嗎?其實,inline 只是建議編譯器這麼做,編譯器究竟會不會這麼做就不一定了。這與編譯器的最佳化級別相關,請看下圖:
gcc 的 -O 選項可以指定最佳化級別,我們上面編譯程式時沒有使用 -O 選項,因此編譯器執行的是預設的 -O0,也即無最佳化編譯。那能否在 -O0 最佳化級別也使用 inline 函式的特性呢?當然是可以的,只需要在定義 inline 函式時,新增 __attribute__((always_inline)) 即可,例如:
現在再來編譯C語言程式並檢視彙編程式碼,得到如下結果:
這種情況下,編譯器並沒有為 i_add() 函式生成響應的彙編程式碼。雖然 inline 函式在預處理階段沒有像 define 宏定義那樣展開,但是在生成彙編程式碼階段展開了,而且參與了呼叫它的程式碼部分的最佳化,這顯然會讓整個C語言程式的效率提高。
小結在 C語言程式開發中,建議把那些對時間要求比較高,且C語言程式碼長度比較短的函式定義為 inline 函式,這麼做常常可以提升程式的效率。在預設的 -O0 編譯最佳化項不能確保 inline 一定起作用,但是可以新增新增 __attribute__((always_inline))強制編譯器對 inline 函式做相應的處理。因為 inline 函式會將自己展開,所以編譯器通常不會再為 inline 生成彙編程式碼,不過,如果是透過函式指標的形式呼叫 inline 函式,編譯器為了獲得 inline 函式的地址,仍然會為其生成彙編程式碼的。