C語言陷阱與技巧第2節,使用inline函數可以提升程序效率,但是讓...

2021-01-08 IT劉小虎

打開 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 函數的地址,仍然會為其生成彙編代碼的。

相關焦點

  • C語言陷阱與技巧第15節,為什麼每調用一次函數,就需要一次if判斷...
    在C語言程序開發中,調用一個有返回值的函數時,一般要對函數的返回值做判斷,以確定函數是否按照預期執行。如果被調用函數沒有按照預期執行,最好加上相應的錯誤處理代碼,否則最終編譯得到的C語言程序穩定性就不夠好,遇到一點點意外,可能就不會正常工作了。沒有判斷C語言函數的返回值,會有什麼問題?
  • C語言陷阱與技巧第8節,輸出適當的信息,有利於定位錯誤和異常代碼
    fun1() 函數和 fun2() 函數都會根據 cond() 函數的返回值做一些進一步的工作(上面的C語言代碼略過了「進一步工作」)。在 main() 函數中「堆積木」調用 fun1() 和 fun2() 函數時,使用了 if 語句判斷它們的返回值,並且根據返回值做了不同的處理。
  • C語言陷阱與技巧第13節,1位元組(Byte)一定等於8位(bit)嗎?C語言操作...
    但是C語言的這些「低級」也是 C語言的優點——使用C語言開發程序,程式設計師能夠準確知道究竟使用了多少資源,以及哪些資源還在內存裡,哪些已經被釋放。換句話說,C語言程序具備資源的使用確定性。因此,C語言特別適合用於一些資源比較匱乏的項目開發中。
  • 言C語言陷阱與技巧第21節,函數只能返回一個值嗎?有多個返回值怎麼...
    如今幾乎找不到只專注於一門程式語言的程式設計師了。大多數程式設計師在自己平時的工作和生活中,一般都使用不止一門程式語言,例如小編在工作中主要使用的是C語言,但是有時候驗證算法也會使用 matlab 和 python,在業餘做別的項目時還會用到 C#。
  • C語言陷阱與技巧31節,都說void*指針是「萬能指針」,它萬能在哪
    在C語言程序開發中,一些比較成熟的庫函數常常會被使用。畢竟,如果手邊就有不錯的「輪子」可以用,沒有程式設計師願意再花費精力憑空造一個輪子出來。例如,在實際的C語言項目開發中,操作某個對象時,常常先構建結構體 struct S 描述該對象,然後使用 init() 函數獲取相應信息,因為接下來的操作函數 handle() 需知道要操作哪個對象,所以要使用 init() 函數返回的信息,C語言代碼似乎可以這麼寫:struct S *p = init();handle(p);從上面兩行
  • Kotlin最佳實踐:在高階函數中使用inline - 碼農登陸
    前言最近,無意中看到一篇文章,是聊inline在高階函數中的性能提升,說實話之前沒有認真關注過這個特性,所以藉此機會好好學習了一番。高階函數:入參中含有lambda的函數(方法)。原文是一位外國小哥寫的,這裡把它翻譯了一下重寫梳理了一遍發出來。
  • Python使用ctypes模塊調用DLL函數之C語言數組與numpy數組傳遞
    在Python語言中,可以使用ctypes模塊調用其它如C++語言編寫的動態連結庫DLL文件中的函數,在提高軟體運行效率的同時,也可以充分利用目前市面上各種第三方的DLL庫函數,以擴充Python軟體的功能及應用領域,減少重複編寫代碼、重複造輪子的工作量,這也充分體現了Python語言作為一種膠水語言所特有的優勢
  • 很多C語言初學者都非常好奇的問題,怎樣定義可以可變參數函數?
    ;}也正因為這段著名的程序,printf() 函數成為大多數C語言初學者接觸到的第一個標準庫函數。foo(),它可以接收類似於 printf() 的函數,並且將 fmt 中的 s 解析為字符串,d 解析為整數,c 解析為字符,因此編譯並執行這段C語言代碼,可得到如下輸出:# gcc t.c# .
  • 深入理解C語言
    但是,你知道這段程序的退出碼嗎?在ANSI-C下,退出碼是一些未定義的垃圾數。但在C89下,退出碼是3,因為其取了printf的返回值。為什麼printf函數返回3呢?因為其輸出了』4′, 』2′,』\n』 三個字符。而在C99下,其會返回0,也就是成功地運行了這段程序。你可以使用gcc的 -std=c89或是-std=c99來編譯上面的程序看結果。
  • C語言中的main()函數可以有好幾種類型,為何都能做入口函數呢?
    main();C++程序基本上也是如此,但是 C++ 提供了重載語法支持,因此同一個函數具有不同的參數類型是可以理解的。而C語言沒有重載語法,為什麼在C語言程序中,可以有不同類型的 main() 函數呢?為什麼在C語言程序中,可以有不同類型的 main() 函數呢?C語言程序支持多種類型 main() 函數,其實和支持可變參數函數是類似的。
  • C語言編程技巧:控制臺程序中自定義函數實現數組內容的特定顯示
    在用C語言編寫算法調試方面的程序中,經常會遇到這種情況,在不同地方需要對處理後的數組內容多次進行顯示,並且很多情況下並非顯示數組裡面的全部內容,而僅僅是想觀察數組中的部分數據內容,若每次顯示時都用printf函數寫的話,未免太過麻煩了。
  • R語言中使用boxplot函數繪製箱線圖
    5個特徵值是變量的最大值、最小值、中位數、第1四分位數和第3四分位數。連接兩個分位數畫出一個箱子,箱子用中位數分割,把兩個極值點與箱子用線條連接,即成箱線圖。R中繪製箱線圖的函數boxplot(1)基本用法boxplot(x, ...)
  • C語言基礎知識學習(一)
    C程序基礎1. 標識符在程序中使用的變量名、函數名、數組名、指針名、標號等稱為標識符.b) 預定義標識符包括C語言提供的庫函數、預編譯處理命令。這類標識符可以另做他用,但將會失去原意,所以一般不另他用。c) 用戶標識符根據需要定義的標識符。一般用來給變量、函數、數組、文件等命名。用戶標誌符如果與C語言的關鍵字重名,系統報錯;若與標準庫函數重名,系統不報錯,但預定義標識符將會失去原意,代之以用戶新定義的含義。
  • R語言中使用par函數在同一繪圖區中繪製多幅圖
    par函數概述在R繪圖時,有時我們想在一個繪圖區中同時繪製多幅圖。在R語言中可以有多個函數來實現此要求。這裡先介紹一下繪圖參數par函數的使用。R中的par()函數可以將繪圖區分割成規則的幾個部分。多圖環境用參數mfrow或參數mfcol來設定,如:par(mforw=c(3,2))則是在同一繪圖區中繪製3行2列共6個圖形,而且是先按行繪製,即繪製完第1行的2個圖形後,再繪製第2行的2個圖形,最後是第3行的2個圖形。同理,par(mfcol=c(3,2))也是繪製3行2列共6個圖形,與上面不同的是,先按列繪製。
  • C語言編程技巧:以實例跟我學動態數組的創建及使用方法
    在C語言中提供了諸如內存的申請、釋放等管理函數,然後結合指針可以按需動態地分配內存空間,來構建動態數組,達到有效利用計算機內存資源的手段。基本函數說明C語言中用於動態數組操作的函數主要包括malloc、calloc、realloc和free等,每個函數的原型、參數意義及功能說明如下表所示:下面通過一個例子演示動態數組的創建及使用方法。
  • R語言中使用lines函數繪製折線圖
    lines函數概述R語言中,abline函數每次僅能繪製一條直線,如果給出若干點,依次用線段連接起來的話,這可以藉助lines函數。R語言中lines函數的使用格式如下:lines(x, y = NULL,...)
  • C語言怎麼樣?今天聊聊C語言的發展史!
    在C語言被用作系統程式語言之前,Tomphson也用過B語言編寫過作業系統。可見在C語言實現以前,B語言已經可以投入實用了。因此第一個C語言編譯器的原型完全可能是用B語言或者混合B語言與PDP彙編語言編寫的。 我們現在都知道,B語言的執行效率比較低,但是如果全部用彙編語言來編寫,不僅開發周期長、維護難度大,更可怕的是失去了高級程序設計語言必需的移植性。
  • C語言——用函數實現模塊化程序設計
    如同組裝計算機,事先生產好各種部件,在最後組裝計算機時,用到什麼就從倉庫裡取出什麼,直接裝上就可以了。絕不會採用手工業方式,在用到電源時臨時生產個電源,用到主板時臨時生產一個主板。這就是模塊化程序設計的思路。function在英文中的意思既是「函數」,也是「功能」。從本質意義上來說,函數就是用來完成一定的功能的。
  • 第五篇:C語言中有關函數的相關知識點梳理
    函數是C語言中,組織程序的最基本的結構單元。我們最初學習C語言的第一個程序就是寫在主函數main()裡面的。在學習函數具體應用之前,我們只認識一個主函數,所有的代碼都必須寫在主函數裡面。如果只是實現一些常規的小功能、小任務,一個主函數加部分系統函數就已經足夠了。但是,如果要編寫的是一個較大的程序,由若干功能模塊組成,且任務特殊。按照人們化繁為簡的思維習慣,大家往往喜歡把一個複雜的問題分解成若干個小任務,當所有的小任務解決了,這個複雜的大問題也就實現了。而且在出現錯誤的時候,也能夠快速發現並及時處理。
  • 如何利用C語言求二元一次方程的解
    那麼在編程前我們先要制定流程圖,二元一次方程的係數在這裡我們依然使用我們常用的a,b,c,根的判別式,這個我們都知道b^2-4ac(其中『^』這個是指數的意思),求解公式等。之後我們要思考,當程序進行判斷時,我們應該用什麼函數進行判斷那,當然了我們可以使用if語句來進行判斷,if語句簡單易懂。好了前期的準備,我們都已經ok了,我們可以來進行編寫的過程了。