Understand the ins and outs of inlining.
Inline函數背後的作法是將「對函數的每個調用」都用函數本體(function body)替換之。其好處是:
- 能夠消除函數調用所帶來的開銷。
- 編譯器最優化機制一般被設計用來濃縮那些「不含函數調用」的代碼,所以當你inline某個函數,或許編譯器有能力對它(函數本體)執行語境相關最優化。大部分編譯器不會爲一個「outlined函數調用」執行這種最優化動做。
然而inline函數這些美好的一面也伴隨着代價:過多的inline替換可能會增長程序的目標碼(object code)大小。在一臺內存有限的機器上,過分inlining會形成加載至內存中的程序體積太大,即便擁有虛擬內存,inline形成的代碼膨脹會致使額外的換頁行爲(paging),下降高速緩存裝置的擊中率,致使效率損失。(若是inline函數的本體很小,編譯器針對「函數本體」產生的目標碼可能比「函數調用」所產生的目標碼更小,那麼inlining確實可能致使更小的程序目標碼和較高的指令高速緩存裝置擊中率。)
但這不是inline的全貌,inline只是對編譯器的一個申請,不是強制命令。這項申請能夠隱式聲明,也能夠顯式聲明。這意味着兩方面:
- 當你申請(隱式或者顯式)對一個函數進行inlining時,編譯器未必真的這麼作了,編譯器本身會根據具體狀況做出判斷。
- 有些你沒注意到的寫法可能致使一個函數被隱式inlining,例如將函數的聲明和實現均放在頭文件中。
1. 隱式的inline申請:在頭文件中聲明class成員函數(或者friend函數)時同時實現該函數。
class person
{
public:
...
int age() {return m_age;} // 該函數會被隱式申請爲inline函數
...
private:
int m_age;
};
這樣的函數一般是成員函數,可是若是把friend函數的實現也放在頭文件內,那麼該friend函數也會被隱式申請爲inline。html
例如:程序員
class dummy
{
public:
explicit dummy(int i) : m_data(i)
{}
private:
int m_data;
friend void swap(dummy& lhs, dummy& rhs)
{
int temp = lhs.m_data;
lhs.m_data = rhs.m_data;
rhs.m_data = temp;
}
};
2. 顯式申請inline函數的作法是在其定義前加上inline關鍵字。
template <typename T>
inline const T& max(const T& a, const T& b)
{return a >b ? a : b;}
3. inline函數與template函數一般都被定義於頭文件內,這會形成誤解:function templates 必定必須是inline。
但這個結論不只無效且可能有害。由於:
inline函數之因此通常被置於頭文件內,那是由於大多數編譯環境(building enviroment)是在編譯過程當中進行inlining,爲了將一個「函數調用」替換爲「被調用函數的本體」,編譯器必須知道該函數的實現內容。雖然有些編譯環境能夠在連接期完成inlining,甚至.NET CLI的託管環境能夠在運行期完成inlining,但大多數C++編譯環境是在編譯期完成inlining行爲的。
一樣,template函數的實現通常被放在頭文件中也是由於模版函數的實例化通常也是在編譯期完成的,於是編譯器須要知道函數的實現內容(某些編譯環境能夠在連接期完成模板實例化,但這不常見)。
所以,template與inline無必然聯繫,若是你想讓根據template函數實例化出的全部函數都應該是inlined,那麼你就須要將此template函數聲明爲inline。不然,你應該避免這麼作,由於inlining須要成本(如引起代碼膨脹等等)。
4. 編譯器拒絕將過於複雜(帶有循環或者遞歸)的函數inlining,而且全部的virtual函數的調用都會使inlining落空。
由於virtual函數意味在運行期才能動態地決定哪個函數被調用,可是inline意味着在編譯器就須要將被函數的調用替換成函數的本體。
5. 另外,若是程序中要取得某個inline函數的地址,編譯器一般會爲此函數生成一個outlined函數本體。
由於編譯器沒有能力產生一個指針指向並不存在的函數。同時,編譯器一般不對「經過函數指針而進行的調用」實施inlining動做,即:對inline函數的調用有可能被inlined,也有可能不被inlined,這取決於該調用是如何實施的:
inline void fn() {…} // 假設編譯器願意inline「對fn的調用」
void (*pf) () = fn; // pf指向fn
...
fn(); // 該調用會被inlined,由於這是一個正常調用。
pf(); // 該調用不會被inlined, 由於它是經過函數指針實施。
因此,一個inline函數是否真的被inlining,取決於編譯器的判斷。
6. 即便程序員本身從未使用函數指針,編譯器有時候仍是會生成構造函數和析構函數的outline副本。
這樣一來它們就能夠得到指針指向那些函數,在array內部元素的構造和析構中使用。
實際上構造函數和析構函數每每是不適合被inlined的。
由於雖然程序員定義了一個空的構造/析構函數,但並不意味着編譯後,該構造/析構函數的實現必定是空的,由於編譯器會在編譯器間產生並安插代碼到構造函數或者析構函數中。
由於C++指出,當建立一個對象時,每個base class及其每個成員變量都會被構造;當銷燬一個對象時,反向的析構動做也會發生。若是有異常在對象構造期間被拋出,該對象已構造好的那一部分會被自動銷燬。這些動做的實現代碼就會又編譯器代爲產生並安插到derived class的構造或者析構函數中。
無論編譯器具體產生了什麼樣的代碼,derived構造函數必定會調用其自身成員變量和base class二者的構造函數,而這些調用會影響編譯器是否對此「空白構造/析構函數」進行inlining。
7. 將函數聲明爲inline還會爲程序開發過程帶來衝擊。
inline函數沒法隨着程序庫的升級而升級,若是fn是程序庫中的一個inline函數,全部調用了fn的「客戶程序」都會將fn函數本體編譯到其程序中,一旦程序庫設計者改變了fn,全部用到fn的「客戶程序」都須要被從新編譯。
若是fn不是inline函數,一旦它有任何修改,「客戶程序」只須要從新連接就好(若是是靜態連接),遠比從新編譯負擔少。若是程序庫使用靜態連接,fn的改動甚至不會被「客戶程序」察覺。
另外,inline函數會給調試帶來麻煩,由於沒法在一個並不存在的函數中設立斷點,從而致使許多編譯環境會選擇在調試版程序(DEBUG)中禁止發生linlining。
- 一個合乎邏輯的策略是:一開始不要講任何函數聲明爲inline,或至少將inlining侷限於小型的、被頻繁調用的函數身上。這會使得往後的調試和二進制升級更容易,也可以使代碼膨脹的問題最小化,使程序的速度提高機會最大化。
- 不要只由於function template出如今頭文件中,就將它們聲明爲inline。
From:http://www.cnblogs.com/xkfz007/archive/2012/03/27/2420166.html緩存
不恰當地使用inline致使編譯器拒絕進行inlining是會帶來反作用的,這會帶來代碼膨脹(目標碼膨脹)和可能極難察覺的bug。由於編譯器對普通函數(沒有聲明爲inline)的實現與對inlining失敗的函數的實現是不一樣的。函數
普通函數在編譯時被單獨編譯爲一個對象,包含在相應的目標文件中。目標文件在連接時,對該函數的調用會被連接到該對象上。優化
若一個函數被聲明爲inline,那麼編譯器即便遇到該函數的聲明也不會爲該函數編譯一個對象,由於inline函數是在調用的地方進行展開的。可是若是在調用的地方發現該inline函數不適合被展開怎麼辦?一種作法是在調用該內聯函數的目標文件中爲該內聯函數編譯一個對象。這麼作的直接後果是:若在多個文件調用了內聯失敗的函數,其中每一個文件對應的目標文件中都會包含一份該內聯函數的目標代碼。ui
若是編譯器真的選擇了上面的作法對待內聯失敗的函數,那麼目標代碼的體積膨脹得與成功內聯的目標代碼同樣,但目標代碼的效率確和沒內聯同樣。spa
更糟的是因爲存在多份函數目標代碼帶來一些程序bug。最明顯的例子是:內聯失敗的函數內的靜態變量實際上就不在只有一份,而是有若干份。這顯然是個錯誤,可是若是不瞭解內幕就很難找到緣由。 設計