內容提要:
1.內聯相對於宏的優點
2.為什麼“inline”只是程式員對編譯器的建議(而非強制命令)
3.內聯失敗的陷阱
在C、C++中函數調用需要少量開銷。有時候這少量開銷積少成多,對程式效能造成影響。有時候函數本身很簡單,函數調用的開銷比執行函數內容本身的開銷還大。C程式員一定知道可以採用宏(Macro)機制來改善上述情況。但是宏基本上是在先行編譯階段做文本替換,因此它有以下缺陷:
1.它無法進行類型檢查;
2.傳入有副作用(side effect)的運算式作為參數可能引起微妙的程式臭蟲;
3.無法單步調試。
4.代碼膨脹。
內聯機制被引入C++作為對宏(Macro)機制的改進和補充(不是取代)。內嵌函式的參數傳遞機制與普通函數相同。但是編譯器會在每處調用內嵌函式的地方將內嵌函式的內容展開。這樣既避免了函數調用的開銷又沒有宏機制的前三個缺陷。
但是程式碼中的關鍵字“inline”只是對編譯器的建議:被“inline”修飾的函數不一定被內聯(但是無“inline”修飾的函數一定不是)。
許多書上都會提到這是因為編譯器比絕大多數程式員都更清楚函數調用的開銷有多大,所以如果編譯器認為調用某函數的開銷相對該函數本身的開銷而言微不足道或者不足以為之承擔代碼膨脹的後果則沒必要內聯該函數。這當然有一定道理,但是按照C、C++一脈相承的賦予程式員充分自由與決定權的風格來看,理由還不夠充分。我猜想最主要的原因是為了避免編譯器陷入無窮遞迴。如果內嵌函式之間存在遞迴調用則可能導致編譯器展開內嵌函式時陷入無窮遞迴。有時候函數的遞迴調用十分隱蔽,程式員並不容易發現,所以簡單起見,將內聯與否的決定權交給編譯器。
另一種不被內聯的情況是使用函數指標來調用內嵌函式。
對於C++中內聯機制的一個常見誤解是:關鍵字“inline“只是對編譯器的建議,如果編譯器發現指定的函數不適合內聯就不會內聯;所以即使內聯使用的不恰當也不會有任何副作用。這句話只對了一半,內聯使用不恰當是會有副作用的:會帶來代碼膨脹,還有可能引入難以發現的程式臭蟲。
根據規範,當編譯器認為希望被內聯的函數不適合內聯的時候,編譯器可以不內聯該函數。但是不內聯該函數不代表該函數就是一個普通函數了,從編譯器的實際實現上來講,內聯失敗函式與普通函數是有區別的:
普通的函數在編譯時間被單獨編譯一個對象,包含在相應的目標檔案中。目標檔案連結時,函數調用被連結到該對象上。
若一個函數被聲明成內嵌函式,編譯器即使遇到該函數的聲明也不會為該函數編譯出一個對象,因為內嵌函式是在用到的地方展開的。可是若在調用該內嵌函式的地方發現該內嵌函式的不適合展開時怎麼辦?一種選擇是在調用該內嵌函式的目標檔案中為該內嵌函式編譯一個對象。這麼做的直接後果是:若在多個檔案調用了內聯失敗函式,其中每個檔案對應的目標檔案中都會包含一份該內嵌函式的目標代碼。
如果編譯器真的選擇了上面的做法對待內聯失敗函式,那麼最好的情況是:沒吃到羊肉,反惹了一身騷。即內聯的好處沒享受到,缺點卻承擔了:目標代碼的體積膨脹得與成功內聯的目標代碼一樣,但目標代碼的效率確和沒內聯一樣。
更糟的是由於存在多份函數目標代碼帶來一些程式臭蟲。最明顯的例子是:內聯失敗函式內的靜態變數實際上就不在只有一份,而是有若干份。這顯然是個錯誤,但是如果不瞭解內幕就很難找到原因。
另:不知道C++ template instantiate機制是否與內聯機制相似,但似乎沒有類似問題,這是為什嗎?