謝邀。
打開Linux內核源代碼,會發現內核在定義C語言函數時,有很多都帶有“inline”關鍵字,請看下圖,那么這個關鍵字有什么作用呢?
inline關鍵字的作用
在C語言程序開發中,inline一般用于定義函數,inline函數也被稱作“內聯函數”,C99和GNUC均支持內聯函數。那么在C語言中,內聯函數和普通函數有什么不同呢?其實,從inline這個名字就應該能看出一點它的性質了——內聯函數會在它被調用的位置上展開,這一點表現的和define宏定義是非常相似的。
將被調用的函數代碼展開,操作系統就無需再在為被調用函數做申請棧幀和回收棧幀的工作,而且,由于編譯器會把被調用的函數代碼和函數本身放在一起優化,所以也有進一步優化C語言代碼,提升效率的可能。
每發生一次函數調用,操作系統就要在程序的??臻g申請一塊內存區域(棧幀),供被調用函數使用,被調用函數執行完畢后,操作系統還要回收這些內存。
不過,天下沒有免費的午餐,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宏那樣展開?,F在使用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語言程序的效率提高。
inline函數雖然表現上很像define宏定義,但是卻并不能完全取代define宏定義,這一點在我之后的文章里會討論,敬請關注。
小結
在C語言程序開發中,建議把那些對時間要求比較高,且C語言代碼長度比較短的函數定義為inline函數,這么做常??梢蕴嵘绦虻男?。在默認的-O0編譯優化項不能確保inline一定起作用,但是可以添加添加__attribute__((always_inline))強制編譯器對inline函數做相應的處理。因為inline函數會將自己展開,所以編譯器通常不會再為inline生成匯編代碼,不過,如果是通過函數指針的形式調用inline函數,編譯器為了獲得inline函數的地址,仍然會為其生成匯編代碼的。
歡迎在評論區一起討論,質疑。文章都是手打原創,每天最淺顯的介紹C語言、linux等嵌入式開發,喜歡我的文章就關注一波吧,可以看到最新更新和之前的文章哦。