1. 程式人生 > >C++ 行內函數 摘自 C++ 應用程式效能優化

C++ 行內函數 摘自 C++ 應用程式效能優化

行內函數

在C++語言的設計中,行內函數的引入可以說完全是為了效能的考慮。因此在編寫對效能要求比較高的C++程式時,非常有必要仔細考量行內函數的使用。 所謂"內聯",即將被呼叫函式的函式體程式碼直接地整個插入到該函式被呼叫處,而不是通過call語句進行。當然,編譯器在真正進行"內聯"時,因為考慮到被行內函數的傳入引數、自己的區域性變數,以及返回值的因素,不僅僅只是進行簡單的程式碼拷貝,還需要做很多細緻的工作,但大致思路如此。
開發人員可以有兩種方式告訴編譯器需要內聯哪些類成員函式,一種是在類的定義體外;一種是在類的定義體內。
(1)當在類的定義體外時,需要在該成員函式的定義前面加"inline"關鍵字,顯式地告訴編譯器該函式在呼叫時需要"內聯"處理,如:

class Student
{
public:
        String  GetName();
        int     GetAge();
        void        SetAge(int ag);
        ……
private:
        String  name;
        int     age;
        ……
};
 
inline String GetName()
{
        return name;
}
 
inline int GetAge()
{
        return age;
}
 
inline void SetAge(int ag)
{
        age = ag;
}

(2)當在類的定義體內且宣告該成員函式時,同時提供該成員函式的實現體。此時,"inline"關鍵字並不是必需的,如:

class Student
{
public:
        String  GetName()       { return name; }
        int     GetAge()        { return age; }
        void        SetAge(int ag)  { age = ag; }
        ……
private:
        String  name;
        int     age;
        ……
};

當普通函式(非類成員函式)需要被內聯時,則只需要在函式的定義時前面加上"inline"關鍵字,如:

    inline int DoSomeMagic(int a, int b)
{
        return a * 13 + b % 4 + 3;
}

因為C++是以"編譯單元"為單位編譯的,而一個編譯單元往往大致等於一個".cpp"檔案。在實際編譯前,前處理器會將"#include"的各標頭檔案的內容(可能會有遞迴標頭檔案展開)完整地拷貝到cpp檔案對應位置處(另外還會進行巨集展開等操作)。前處理器處理後,編譯真正開始。一旦C++編譯器開始編譯,它不會意識到其他cpp檔案的存在。因此並不會參考其他cpp檔案的內容資訊。聯想到內聯的工作是由編譯器完成的,且內聯的意思是將被呼叫行內函數的函式體程式碼直接代替對該行內函數的呼叫。這也就意味著,在編譯某個編譯單元時,如果該編譯單元會呼叫到某個行內函數,那麼該行內函數的函式定義(即函式體)必須也包含在該編譯單元內。因為編譯器使用行內函數體程式碼替代行內函數呼叫時,必須知道該行內函數的函式體程式碼,而且不能通過參考其他編譯單元資訊來獲得這一資訊。

如果有多個編譯單元會呼叫到某同一個行內函數,C++規範要求在這多個編譯單元中該行內函數的定義必須是完全一致的,這就是"ODR"(one-definition rule)原則。考慮到程式碼的可維護性,最好將行內函數的定義放在一個頭檔案中,用到該行內函數的各個編譯單元只需#include該標頭檔案即可。進一步考慮,如果該行內函數是一個類的成員函式,這個標頭檔案正好可以是該成員函式所屬類的宣告所在的標頭檔案。這樣看來,類成員行內函數的兩種宣告可以看成是幾乎一樣的,雖然一個是在類外,一個在類內。但是兩個都在同一個標頭檔案中,編譯器都能在#include該標頭檔案後直接取得行內函數的函式體程式碼。討論完如何宣告一個行內函數,來檢視編譯器如何內聯的。繼續上面的例子,假設有個foo函式:

#include "student.h"
...
 
void foo()
{
        ...
        Student abc;
        abc.SetAge(12);
        cout << abc.GetAge();
        ...
}

foo函式進入foo函式時,從其棧幀中開闢了放置abc物件的空間。進入函式體後,首先對該處空間執行Student的預設建構函式構造abc物件。然後將常數12壓棧,呼叫abc的SetAge函式(開闢SetAge函式自己的棧幀,返回時回退銷燬此棧幀)。緊跟著執行abc的GetAge函式,並將返回值壓棧。最後呼叫cout的<<操作符操作壓棧的結果,即輸出。

#include "student.h"
...
 
void foo()
{
        ...
        Student abc;
        {
            abc.age = 12;
        }
        int tmp = abc.age;
        cout << tmp;
        ...
}

這時,函式呼叫時的引數壓棧、棧幀開闢與銷燬等操作不再需要,而且在結合這些程式碼後,編譯器能進一步優化為如下結果:

#include "student.h"
...
 
void foo()
{
        ...
        cout << 12;
        ...
}

這顯然是最好的優化結果;相反,考慮原始版本。如果SetAge/GetAge沒有被內聯,因為非行內函數一般不會在標頭檔案中定義,這兩個函式可能在這個編譯單元之外的其他編譯單元中定義。即foo函式所在編譯單元看不到SetAge/GetAge,不知道函式體程式碼資訊,那麼編譯器傳入12給SetAge,然後用GetAge輸出。在這一過程中,編譯器不能確信最後GetAge的輸出。因為編譯這個編譯單元時,不知道這兩個函式的函式體程式碼,因而也就不能做出最終版本的優化。

從上述分析中,可以看到使用行內函數至少有如下兩個優點。

(1)減少因為函式呼叫引起開銷,主要是引數壓棧、棧幀開闢與回收,以及暫存器儲存與恢復等。

(2)內聯後編譯器在處理呼叫行內函數的函式(如上例中的foo()函式)時,因為可供分析的程式碼更多,因此它能做的優化更深入徹底。前一條優點對於開發人員來說往往更顯而易見一些,但往往這條優點對最終程式碼的優化可能貢獻更大。