內聯函數(inline)機制與陷阱

內容提要:

1.內聯相對於宏的優點

2.爲什麼“inline”只是程序員對編譯器的建議(而非強制命令)

3.內聯失敗的陷阱

CC++中函數調用需要少量開銷。有時候這少量開銷積少成多,對程序性能造成影響。有時候函數本身很簡單,函數調用的開銷比執行函數內容本身的開銷還大。C程序員一定知道可以採用宏(Macro)機制來改善上述情況。但是宏基本上是在預編譯階段做文本替換,因此它有以下缺陷:

1.它無法進行類型檢查;

2.傳入有副作用(side effect)的表達式作爲參數可能引起微妙的程序臭蟲;

3.無法單步調試。

4.代碼膨脹。

內聯機制被引入C++作爲對宏(Macro)機制的改進和補充(不是取代)。內聯函數的參數傳遞機制與普通函數相同。但是編譯器會在每處調用內聯函數的地方將內聯函數的內容展開。這樣既避免了函數調用的開銷又沒有宏機制的前三個缺陷。

但是程序代碼中的關鍵字“inline”只是對編譯器的建議:被“inline”修飾的函數不一定被內聯(但是無“inline”修飾的函數一定不是)。

許多書上都會提到這是因爲編譯器比絕大多數程序員都更清楚函數調用的開銷有多大,所以如果編譯器認爲調用某函數的開銷相對該函數本身的開銷而言微不足道或者不足以爲之承擔代碼膨脹的後果則沒必要內聯該函數。這當然有一定道理,但是按照CC++一脈相承的賦予程序員充分自由與決定權的風格來看,理由還不夠充分。我猜想最主要的原因是爲了避免編譯器陷入無窮遞歸。如果內聯函數之間存在遞歸調用則可能導致編譯器展開內聯函數時陷入無窮遞歸。有時候函數的遞歸調用十分隱蔽,程序員並不容易發現,所以簡單起見,將內聯與否的決定權交給編譯器。

另一種不被內聯的情況是使用函數指針來調用內聯函數。


對於C++中內聯機制的一個常見誤解是:關鍵字“inline“只是對編譯器的建議,如果編譯器發現指定的函數不適合內聯就不會內聯;所以即使內聯使用的不恰當也不會有任何副作用。這句話只對了一半,內聯使用不恰當是會有副作用的:會帶來代碼膨脹,還有可能引入難以發現的程序臭蟲。


根據規範,當編譯器認爲希望被內聯的函數不適合內聯的時候,編譯器可以不內聯該函數。但是不內聯該函數不代表該函數就是一個普通函數了,從編譯器的實際實現上來講,內聯失敗的函數與普通函數是有區別的:

普通的函數在編譯時被單獨編譯一個對象,包含在相應的目標文件中。目標文件鏈接時,函數調用被鏈接到該對象上。

若一個函數被聲明成內聯函數,編譯器即使遇到該函數的聲明也不會爲該函數編譯出一個對象,因爲內聯函數是在用到的地方展開的。可是若在調用該內聯函數的地方發現該內聯函數的不適合展開時怎麼辦?一種選擇是在調用該內聯函數的目標文件中爲該內聯函數編譯一個對象。這麼做的直接後果是:若在多個文件調用了內聯失敗的函數,其中每個文件對應的目標文件中都會包含一份該內聯函數的目標代碼。

如果編譯器真的選擇了上面的做法對待內聯失敗的函數,那麼最好的情況是:沒吃到羊肉,反惹了一身騷。即內聯的好處沒享受到,缺點卻承擔了:目標代碼的體積膨脹得與成功內聯的目標代碼一樣,但目標代碼的效率確和沒內聯一樣。

更糟的是由於存在多份函數目標代碼帶來一些程序臭蟲。最明顯的例子是:內聯失敗的函數內的靜態變量實際上就不在只有一份,而是有若干份。這顯然是個錯誤,但是如果不瞭解內幕就很難找到原因。

另:不知道C++ template instantiate機制是否與內聯機制相似,但似乎沒有類似問題,這是爲什麼?
發佈了107 篇原創文章 · 獲贊 12 · 訪問量 60萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章