1. 程式人生 > >小問題大思考之C++裡的inline函式

小問題大思考之C++裡的inline函式

inline,一個神奇的關鍵字。有了它,你同時就可以獲取函式和巨集的優點。inline定義的函式,比起沒有inline的函式來說,沒有執行函式呼叫所帶來的負擔(對此可參見《C++程式的記憶體佈局》),因此它是高效率的;比起巨集來,它具有函式的可預期行為和引數型別檢驗。巨集的行為難於預期,我們看看下面這個巨集定義

#define max(a, b) ( (a) > (b) ? (a) : (b) )

int a = 5, b = 0;
max(++a, b); // a = a + 2
max(++a, b+10); // a = a + 1

如果這樣:
inline int max(int a, int b)
{
     return a > b ? a : b;
}

int a = 5, b = 0;
max(++a, b); // a = a + 1
max(++a, b+10); // a = a + 1

一切都很美好!但是會這麼簡單嗎?

C++最初引入inline的原因是不想破壞類的封裝,同時保持高效率。例如:

class stack {
private:
  int i;
  
public:
  int get() {return i;} // inline函式
}; 

想訪問stack的成員變數i,想保持stack的封裝,同時還想呼叫時高效率,那麼請inline。

inline對於編譯器而言,意味著“在編譯階段,將呼叫動作以被呼叫函式的本體替換之”。但是它只是一種建議,編譯器可以去做,也可以不去做。從邏輯上來說,編譯器將函式inline的步驟如下:

1、將inline函式體複製到inline函式呼叫點處;

2、為所用inline函式中的區域性變數分配記憶體;

3、將inline函式的的輸入引數和返回值對映到呼叫方法的區域性變數空間中;

4、如果inline函式有多個返回點,將其轉變為inline函式程式碼塊末尾的分支(使用GOTO)。

經過以上處理,可消除所有與呼叫相關的痕跡以及效能的損失。inline通過消除呼叫開銷來提升效能,並且允許進行呼叫間優化。我們看下面這段程式碼:

int test(){
  int a = 6;
  ...... // 此處省略程式碼未對a經行修改
  int b = inline_func(b);
  ...... // 此處省略程式碼未對b經行修改
  int c = b + 1;
  ......
}

inline int inline_func(int q) {
  if (q > 10) return -1;
  else if (q > 0) return (1 << q) - 1;
  else return 0;
}
inline後
int test() {
  int a = 6;
  ...... // 此處省略程式碼未對a經行修改
  int b;
  {
    int _temp_q = 6;
    int _temp;
    if (_temp_q > 10) _temp = -1;
    else if (_temp_q > 0) _temp = (1 << q) - 1;
    else _temp = 0;
    b = _temp;
  }
  ...... // 此處省略程式碼未對b經行修改
  int c = b + 1;
  ......
}
優化後
int test(){
  int a = 6;
  ...... // 此處省略程式碼未對a經行修改
  int b = 0x3f;
  ...... // 此處省略程式碼未對b經行修改
  int c = 0x40;
  ......
}

上面我們主要說了inline函式的優點,那麼inline函式的缺點有哪些呢?我們來看看:

1、程式碼膨脹。如果inline函式體過大且編譯器還讓它inline成功,那麼你最終的程式會程式碼膨脹,從而造成裝置緩衝命中率低,引起較多的頁面錯誤,讀寫硬碟的次數增多,這樣程式的效能就下降了!建議:inline函式體一般不要超過5行,不包括迴圈,不包括遞迴呼叫。

2、inline函式內部不要有static變數。inline函式的定義幾乎總是放在標頭檔案(.h)裡,這允許多個實現檔案(.cpp)得以引用。我們知道編譯器是分別編譯的,所以這個時候,在多個實現檔案裡就會有多個inline函式的展開,也就是說有個多個static變數,這恐怕不是我們期望的!

3、inline函式無法隨著函式庫升級而升級。如果f是函式庫中的一個inline函式,使用它的使用者會將f函式實體編譯到他們的程式中。一旦函式庫實現者改變f,所有用到f的程式都必須重新編譯。如果f是non-inline的,使用者程式只需重新連線即可。如果函式庫採用的是動態連線,那這一升級的f函式可以不知不覺的被程式使用。

4、不要獲取inline函式的地址。如果要取得一個inline函式的地址,編譯器就必須為此函式產生一個函式實體,無論如何,編譯器無法交出一個“不存在函式”的指標。注意,有些編譯器可能會使用類的constructors和destructors的函式指標,用以構造和析構一個class物件的陣列。另外類的constructors和destructors可能簡單,但是其父類的類的constructors和destructors可能是複雜的,所以類的constructors和destructors往往不是inline函式的最佳選擇

5、inline虛擬函式往往是無效的。虛擬函式往往是執行時確定的,而inline是在編譯時進行的,所以inline虛擬函式往往無效。當然如果直接用類的物件來使用虛擬函式,那麼對有的編譯器而言,也可起到優化的作用。

6、inline函式無法除錯。原因請參見上面編譯器將函式inline的步驟。所以請在專案後期,對程式進行profile後,再決定將那些函式inline化。


參考文獻:

1、《C++語言的設計和演化》2.4 執行時的效率

2、《Effective C++》 條款33

3、《提高C++效能的程式設計技術》 第8章 內聯基礎,第9章 內聯-站在效能的角度, 第10章 內聯技巧