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函數:code

#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)減小由於函數調用引發開銷,主要是參數壓棧、棧幀開闢與回收,以及寄存器保存與恢復等。it

(2)內聯後編譯器在處理調用內聯函數的函數(如上例中的foo()函數)時,由於可供分析的代碼更多,所以它能作的優化更深刻完全。前一條優勢對於開發人員來講每每更顯而易見一些,但每每這條優勢對最終代碼的優化可能貢獻更大。

相關文章
相關標籤/搜索