[Effective C++系列]-透徹瞭解inlining的裡裡外外

來源:互聯網
上載者:User

標籤:

Understand the ins and outs of inlining. 
  • [原理]
Inline函數背後的做法是將“對函數的每一個調用”都用函數本體替換之。其好處是:
  1. 可以消除函數調用所帶來的開銷。
  2. 編譯器最佳化機制通常被設計用來濃縮那些“不含函數調用”的代碼,因此當你inline某個函數,或許編譯器有能力對它(函數本體)執行語境相關最佳化。大部分編譯器不會為一個“outlined函數調用”執行這種最佳化動作。
然而inline函數這些美好的一面也伴隨著代價:inline函數可能增加程式的目標碼(object code)大小。在一台記憶體有限的機器上,過度inlining會造成載入至記憶體中的程式體積太大,即使擁有虛擬記憶體,inline造成的代碼膨脹會導致額外的換頁行為(paging),降低快取裝置的擊中率,導致效率損失。(如果inline函數的本體很小,編譯器針對“函數本體”產生的目標碼可能比“函數調用”所產生的目標碼更小,那麼inlining確實可能導致更小的程式目標碼和較高的指令快取裝置擊中率。) 但這不是inline的全貌,inline只是對編譯器的一個申請,不是強制命令。這項申請可以隱式聲明,也可以顯式聲明。這意味著兩方面:
  1. 當你申請(隱式或者顯式)對一個函數進行inlining時,編譯器未必真的這麼做了。
  2. 有些你沒注意到的寫法可能導致一個函數被隱式inlining。

 

  • [詳解]
1. 隱式的inline申請:在標頭檔中聲明class成員函數時同時實現該成員函數。
class person{public:     ...     int age() {return m_age;} // 該函數會被隱式申請為inline函數     ...private:     int m_age;};

這樣的函數通常是成員函數,但是friend函數也可以被定義於class內,那麼這些函數也會被隱式申請為inline。

 2. 顯式申請inline函數的做法是在其定義前加上inline關鍵字。
template <typename T>inline const T&  max(const T& a, const T& b){return a >b ? a : b;}
inline函數與template函數通常都被定義於標頭檔內,這會造成誤解:function templates 一定必須是inline。這個結論不僅無效且可能有害。因為: inline函數之所以一般被置於標頭檔內,那是因為大多數編譯環境(building enviroment)是在編譯過程中進行inlining,為了講一個“函數調用”替換為“被調用函數的本體”,編譯器必須知道該函數的實現內容。雖然有些編譯環境可以在連結期完成inlining,甚至.NET CLI的託管環境可以在運行期完成inlining,但大多數C++編譯環境是在編譯期完成inlining行為的。 同樣,template函數的實現一般被放在標頭檔中也是因為,模版函數的執行個體化一般也是在編譯期完成的,因而編譯器需要知道函數的實現內容(某些編譯環境可以在連結期完成模板執行個體化,但這不常見)。 因此,template與inline無必然聯絡,如果你想讓根據template函數執行個體化出的所有函數都應該是inlined,那麼你就需要將此template函式宣告為inline。否則,你應該避免這麼做,因為inlining需要成本(如引發代碼膨脹等等)。 3. 編譯器拒絕將過於複雜(帶有迴圈或者遞迴)的函數inlining,並且所有的virtual函數的調用都會使inlining落空。因為virtual函數意味在運行期才能動態地決定哪一個函數被調用,但是inline意味著在編譯器就需要將被函數的調用替換成函數的本體。 4. 另外,如果程式中要取得某個inline函數的地址,編譯器通常會為此函數產生一個outlined函數本體。因為編譯器沒有能力產生一個指標指向並不存在的函數。同時,編譯器通常不對“通過函數指標而進行的調用”實施inlining動作,即對inline函數的調用有可能被inlined,也有可能不被inlined,這取決於該調用是如何實施的:
inline void fn() {…} // 假設編譯器願意inline“對fn的調用”void (*pf) () = fn; // pf指向fn...fn(); // 該調用會被inlined,因為這是一個正常調用。pf(); // 該調用不會被inlined, 因為它是通過函數指標實施。
 所以,一個inline函數是否真的被inlining,取決於編譯器的判斷。 5. 即使程式員自己從未使用函數指標,編譯器有時候還是會產生建構函式和解構函式的outline副本,這樣一來它們就可以獲得指標指向那些函數,在array內部元素的構造和析構中使用。 6. 實際上建構函式和解構函式往往是不適合被inlined的。因為C++指出,當建立一個對象時,每一個base class及其每一個成員變數都會被構造;當銷毀一個對象時,反向的析構動作也會發生。如果有異常在物件建構期間被拋出,該對象已構造好的那一部分會被自動銷毀。 這意味著雖然定義了一個空的建構函式,但並不意味著編譯後,該建構函式的實現一定是空的,因為編譯器會在編譯器間產生並安插代碼到建構函式或者解構函式中。 7. 將函式宣告為inline還會為程式開發過程帶來衝擊。 inline函數無法隨著程式庫的升級而升級,如果fn是程式庫中的一個inline函數,所有調用了fn的“客戶程式”都會將fn函數本體編譯到其程式中,一旦程式庫設計者改變了fn,所有用到fn的“客戶程式”都需要被重新編譯。如果fn不是inline函數,一旦它有任何修改,“客戶程式”只需要重新連結就好(如果是靜態連結),遠比重新編譯負擔少。如果程式庫使用靜態連結,fn的改動甚至不會被“客戶程式”察覺。 另外,inline函數會給調試帶來麻煩,因為無法在一個並不存在的函數中設立斷點,從而導致許多編譯環境會選擇在調試版程式(DEBUG)中禁止發生linlining。 
  • [總結]
  1. 一個合乎邏輯的策略是:一開始不要講任何函式宣告為inline,或至少將inlining局限於小型的、被頻繁調用的函數身上。這會使得日後的調試和二進位升級更容易,也可使代碼膨脹的問題最小化,使程式的速度提升機會最大化。
  2. 不要只因為function template出現在標頭檔中,就將它們聲明為inline。

 

[Effective C++系列]-透徹瞭解inlining的裡裡外外

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.