在C++語言的設計中,內聯函數的引入能夠說徹底是爲了性能的考慮。所以在編寫對性能要求比較高的C++程序時,很是有必要仔細考量內聯函數的使用。html
所謂「內 聯」,即將被調用函數的函數體代碼直接地整個插入到該函數被調用處,而不是經過call語句進行。固然,編譯器在真正進行「內聯」時,由於考慮到被內聯函 數的傳入參數、本身的局部變量,以及返回值的因素,不只僅只是進行簡單的代碼拷貝,還須要作不少細緻的工做,但大體思路如此。函數
開發人員能夠有兩種方式告訴編譯器須要內聯哪些類成員函數,一種是在類的定義體外;一種是在類的定義體內。性能
(1)當在類的定義體外時,須要在該成員函數的定義前面加「inline」關鍵字,顯式地告訴編譯器該函數在調用時須要「內聯」處理,如:優化
class Student設計
{指針
public:htm
String GetName();對象
int GetAge();blog
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()函數)時,由於可供分析的代碼更多,所以它能作的優化更深刻完全。前一條優勢對於開發人員來講每每更顯而易見一些,但每每這條優勢對最終代碼的優化可能貢獻更大。
這時,有必要簡單介紹函數調用時都須要執行哪些操做,這樣能夠幫助分析一些函數調用相關的問題。假設下面代碼:
void foo()
{
...
i = func(a, b, c); ①
... ②
}
調用者(這裏是foo)在調用前須要執行以下操做。
(1)參數壓棧:這裏是a、b和c。壓棧時通常都是按照逆序,所以是c->b->c。若是a、b和c有對象,則須要先進行拷貝構造(前面章節已經討論)。
(2)保存返回地址:即函數調用結束返回後接着執行的語句的地址,這裏是②處語句的地址。
(3)保存維護foo函數棧幀信息的寄存器內容:如SP(堆棧指針)和FP(棧幀指針)等。到底保存哪些寄存器與平臺相關,可是每一個平臺確定都會有對應的寄存器。
(4)保 存一些通用寄存器的內容:由於有些通用寄存器會被全部函數用到,因此在foo調用func以前,這些寄存器可能已經放置了對foo有用的信息。這些寄存器 在進入func函數體內執行時可能會被func用到,從而被覆寫。所以foo在調用func前保存一份這些通用寄存器的內容,這樣在func返回後能夠恢 復它們。
接着調用func函數,它首先經過移動棧指針來分配全部在其內部聲明的局部變量所需的空間,而後執行其函數體內的代碼等。
最後當func執行完畢,函數返回時,foo函數還須要執行以下善後處理。
(1)恢復通用寄存器的值。
(2)恢復保存foo函數棧幀信息的那些寄存器的值。
(3)經過移動棧指針,銷燬func函數的棧幀,
(4)將保存的返回地址出棧,並賦給IP寄存器。
(5)經過移動棧指針,回收傳給func函數的參數所佔用的空間。
在前面章節中已經討論,若是傳入參數和返回值爲對象時,還會涉及對象的構造與析構,函數調用的開銷就會更大。尤爲是當傳入對象和返回對象是複雜的大對象時,更是如此。
由於函數調用的準備與善後工做最終都是由機器指令完成的,假設一個函數以前的準備工做與以後的善後工做的指令所需的空間爲SS,執行這些代碼所需的時間爲TS,如今能夠更細緻地從空間與時間兩個方面來分析內聯的效果。
(1)在 空間上,通常印象是不採用內聯,被調用函數的代碼只有一份,調用它的地方使用call語句引用便可。而採用內聯後,該函數的代碼在全部調用其處都有一份拷 貝,所以最後總的代碼大小比採用內聯前要大。但事實不老是這樣的,若是一個函數a的體代碼大小爲AS,假設a函數在整個程序中被調用了n次,不採用內聯 時,對a的調用只有準備工做與善後工做兩處會增長最後的代碼量開銷,即a函數相關的代碼大小爲:n * SS + AS。採用內聯後,在各處調用點都須要將其函數體代碼展開,即a函數相關的代碼大小爲n * AS。這樣比較兩者的大小,即比較(n * SS + AS)與(n*AS)的大小。考慮到n通常次數不少時,能夠簡化成比較SS與AS的大小。這樣能夠得出大體結論,若是被內聯函數本身的函數體代碼量比由於 函數調用的準備與善後工做引入的代碼量大,內聯後程序的代碼量會變大;相反,當被內聯函數的函數體代碼量比由於函數調用的準備與善後工做引入的代碼量小, 內聯後程序的代碼量會變小。這裏尚未考慮內聯的後續狀況,即編譯器可能由於得到的信息更多,從而對調用函數的優化作得更深刻和完全,導致最終的代碼量變 得更小。
(2)在 時間上,通常而言,每處調用都再也不須要作函數調用的準備與善後工做。另外內聯後,編譯器在作優化時,看到的是調用函數與被調用函數連成的一大塊代碼。即獲 得的代碼信息更多,此時它對調用函數的優化能夠作得更好。最後還有一個很重要的因素,即內聯後調用函數體內須要執行的代碼是相鄰的,其執行的代碼都在同一 個頁面或連續的頁面中。若是沒有內聯,執行到被調用函數時,須要跳到包含被調用函數的內存頁面中執行,而被調用函數所屬的頁面極有可能當時不在物理內存 中。這意味着,內聯後能夠下降「缺頁」的概率,知道減小「缺頁」次數的效果遠比減小一些代碼量執行的效果。另外即便被調用函數所在頁面可能也在內存中,但 是由於與調用函數在空間上相隔甚遠,因此可能會引發「cache miss」,從而下降執行速度。所以總的來講,內聯後程序的執行時間會比沒有內聯要少。即程序的速度更快,這也是由於內聯後代碼的空間 「locality」特性提升了。但正如上面分析空間影響時提到的,當AS遠大於SS,且n很是大時,最終程序的大小會比沒有內聯時要大不少。代碼量大意 味着用來存放代碼的內存頁也會更多,這樣由於執行代碼而引發的「缺頁」也會相應增多。若是這樣,最終程序的執行時間可能會由於大量的「缺頁」而變得更多, 即程序的速度變慢。這也是爲何不少編譯器對於函數體代碼不少的函數,會拒絕對其進行內聯的請求。即忽略「inline」關鍵字,而對如同普通函數那樣編 譯。
綜合上面的分析,在採用內聯時須要內聯函數的特徵。好比該函數本身的函數體代碼量,以及程序執行時可能被調用的次數等。固然,判斷內聯效果的最終和最有效的方法仍是對程序的大小和執行時間進行實際測量,而後根據測量結果來決定是否應該採用內聯,以及對哪些函數進行內聯。
以下根據內聯的本質來討論與其相關的一些其餘特色。
如前所 述,由於調用內聯函數的編譯單元必須有內聯函數的函數體代碼信息。又由於ODR規則和考慮到代碼的可維護性,因此通常將內聯函數的定義放在一個頭文件中, 而後在每一個調用該內聯函數的編譯單元中#include該頭文件。如今考慮這種狀況,即在一個大型程序中,某個內聯函數由於很是通用,而被大多數編譯單元 用到對該內聯函數的一個修改,就會引發全部用到它的編譯單元的從新編譯。對於一個真正的大型程序,從新編譯大部分編譯單元每每意味着大量的編譯時間。所以 內聯最好在開發的後期引入,以免可能沒必要要的大量編譯時間的浪費。
再考慮這 種狀況,若是某開發小組在開發中用到了第三方提供的程序庫,而這些程序庫中包含一些內聯函數。由於該開發小組的代碼中在用到第三方提供的內聯函數處,都是 將該內聯函數的函數體代碼拷貝到調用處,即該開發小組的代碼中包含了第三方提供代碼的「實現」。假設這個第三方單位在下一個版本中修改了某些內聯函數的定 義,那麼雖然這個第三方單位並無修改任何函數的對外接口,而只是修改了實現,該開發小組要想利用這個新的版本,仍然須要從新編譯。考慮到可能該開發小組 的程序已經發布,那麼這種從新編譯的成本會至關高;相反,若是沒有內聯,而且仍然只是修改實現,那麼該開發小組沒必要從新編譯便可利用新的版本。
由於內聯的本質就是用函數體代碼代替對該函數的調用,因此考慮遞歸函數,如:
[inline] int foo(int n)
{
...
return foo(n-1);
}
若是編譯器編譯某個調用此函數的編譯單元,如:
void func()
{
...
int m = foo(n);
...
}
考慮以下兩種狀況。
(1)若是在編譯該編譯單元且調用foo時,提供的參數n不能知道其實際值,則編譯器沒法知道對foo函數體進行多少次代替。在這種狀況下,編譯器會拒絕對foo函數進行內聯。
(2)若是在編譯該編譯單元且調用foo時,提供的參數n可以知道其實際值,則編譯器可能會視n值的大小來決定是否對foo函數進行內聯。由於若是n很大,內聯展開可能會使最終程序的大小變得很大。
如前所 述,由於內聯函數是編譯期行爲,而虛擬函數是執行期行爲,所以編譯器通常會拒絕對虛擬函數進行內聯的請求。可是事情總有例外,內聯函數的本質是編譯器編譯 調用某函數時,將其函數體代碼代替call調用,即內聯的條件是編譯器可以知道該處函數調用的函數體。而虛擬函數不可以被內聯,也是由於在編譯時通常來講 編譯器沒法知道該虛擬函數究竟是哪個版本,即沒法肯定其函數體。可是在兩種狀況下,編譯器是可以知道虛擬函數調用的真實版本的,所以虛擬函數能夠被內 聯。
其一是經過對象,而不是指向對象的指針或者對象的引用調用虛擬函數,這時編譯器在編譯期就已經知道對象的確切類型。所以會直接調用肯定的某虛擬函數實現版本,而不會產生「動態綁定」行爲的代碼。
其二是雖 然是經過對象指針或者對象引用調用虛擬函數,可是編譯時編譯器能知道該指針或引用對應到的對象的確切類型。好比在產生的新對象時作的指針賦值或引用初始 化,發生在於經過該指針或引用調用虛擬函數同一個編譯單元而且兩者之間該指針沒有被改變賦值使其指向到其餘不能確切知道類型的對象(由於引用不能修改綁 定,所以無此之虞)。此時編譯器也不會產生動態綁定的代碼,而是直接調用該肯定類型的虛擬函數實現版本。
在這兩種狀況下,編譯器可以將此虛擬函數內聯化,如:
inline virtual int x::y (char* a)
{
...
}
void z (char* b)
{
x_base* x_pointer = new x(some_arguments_maybe);
x x_instance(maybe_some_more_arguments);
x_pointer->y(b);
x_instance.y(b);
固然在實際開發中,經過這兩種方式調用虛擬函數時應該很是少,由於虛擬函數的語義是「經過基類指針或引用調用,到真正運行時才決定調用哪一個版本」。
從上面的 分析中已經看到,編譯器並不老是尊重「inline」關鍵字。即便某個函數用「inline」關鍵字修飾,並不可以保證該函數在編譯時真正被內聯處理。因 此與register關鍵字性質相似,inline僅僅是給編譯器的一個「建議」,編譯器徹底能夠視實際狀況而忽略之。
另外從內 聯,即用函數體代碼替代對該函數的調用這一本質看,它與C語言中的函數宏(macro)極其類似,可是它們之間也有本質的區別。即內聯是編譯期行爲,宏是 預處理期行爲,其替代展開由預處理器來作。也就是說編譯器看不到宏,更不可能處理宏。另外宏的參數在其宏體內出現兩次或兩次以上時常常會產生反作用,尤爲 是當在宏體內對參數進行++或--操做時,而內聯不會。還有,預處理器不會也不能對宏的參數進行類型檢查。而內聯由於是編譯器處理的,所以會對內聯函數的 參數進行類型檢查,這對於寫出正確且魯棒的程序,是一個很大的優點。最後,宏確定會被展開,而用inline關鍵字修飾的函數不必定會被內聯展開。
最後順帶說起,一個程序的唯一入口main()函數確定不會被內聯化。另外,編譯器合成的默認構造函數、拷貝構造函數、析構函數,以及賦值運算符通常都會被內聯化。
轉自:http://blog.sina.com.cn/s/blog_4a0e545d01000c4e.html