C++那些細節--inline關鍵字
一.簡介
inline是個好東西,不過要注意不能亂用。在項目中看到過許多inline相關的宏定義,_forceinline,_inline等等,有許多有疑惑的地方。於是,本人強迫症發作,決定總結一下inline相關的知識。主要涉及到inline的功能,使用,以及forceinline等。還有類中的virtual函數是否會被inline等問題。
二.inline關鍵字
1.inline優點
inline是標准C++中提供的關鍵字,使用這個關鍵字,意思就是告訴編譯器,介個函數是個內聯函數。內聯函數又是啥呢,簡單的說就是直接將函數的內容替換到調用的位置。這樣做,可以大大的省掉調用函數的性能損失。內聯函數從源代碼層看,有函數的結構,而在編譯後,卻不具備函數的性質。內聯函數在使用時,沒有函數調用的性能損失。但是卻可以像函數一樣,有相關的類型檢查等,我們完全可以按照普通函數那樣對待內聯函數。
看一個例子,比如我們的類中有一個成員變量,我們在使用這個成員變量的時候,為了封裝性,需要使用函數來存取這個變量。
class Test
{
private:
int m_iID;
public:
void SetID(int id);
int GetID();
};
void Test::SetID(int id)
{
m_iID = id;
}
int Test::GetID()
{
return m_iID;
}
但是,我們這樣做的話,本來一下子就可以搞定的事情,卻需要通過函數來實現。雖然這樣有利於封裝,為了更嚴謹的結構,我們寫的時候麻煩一些也就罷了。但是,在運行時,這樣做會造成很大的調用函數的損失。如果調用成千上萬次的話,每次調用都需要進行參數壓棧,保存運行狀態,將結果返回等一系列操作,而我們的函數這麼短小,使用普通函數就有些得不償失了,這時候,把它內聯了可能是最好的選擇。內聯之後,我們就可以在編程的時候將它按照函數處理,而在使用的時候又沒有多少性能開銷,何樂而不為呢?
2.inline缺點
既然inline這麼好,那麼我們是不是可以任意使用inline函數呢? 凡事有一利必有一弊,inline雖然能夠很好的提升性能,但是,它的實現是通過將每一個函數的調用都以函數本身來替代進行實現的。這樣做會有一些弊端。 (1)程序體積增大:函數調用次數越多,我們插入的內容就越多,代碼體積勢必會增大。程序體積增大的話,會導致額外的虛擬內存換頁等行為,降低指令高速緩存的命中率。這樣的話,inline反倒降低了效率。 (2)不利於動態升級。inline函數一般都在.h文件中,將聲明和定義寫在一起,普通函數修改函數內部內容時只需要重新連接就可以,但是內聯函數如果修改的話,所有用到該頭文件的內容都需要重新編譯。 (3)不利於調試,內聯函數的實現是拷貝過去的話,那麼真正調用的時候,那裡根本就不是一個函數,我們沒有辦法去調試一個內聯函數。所以,編譯器在編譯debug版本的時候,是不會將內聯函數inline處理的,而是當做一個普通的函數來處理,這樣我們就可以調試了。
3.怎樣進行inline
inline這麼好,那麼腫麼進行inline操作呢?inline有兩種形式,一種是顯式inline,另一種是隱式inline。 顯式inline顧名思義,就是直接使用inline關鍵字,指定一個函數是inline的。這種情況inline一般是放在類的外面,函數聲明中沒有寫inline關鍵字,在函數定義中添加inline關鍵字。比如:
class Test
{
private:
int m_iID;
public:
void SetID(int id);
int GetID();
};
inline void Test::SetID(int id)
{
m_iID = id;
}
inline int Test::GetID()
{
return m_iID;
}
不過呢,這種inline貌似不是太常用。一般都是使用隱式inline的。當我們把函數的定義放在類的內部時,這個函數就會被隱式inline,我們不需要再寫inline關鍵字了。
class Test
{
private:
int m_iID;
public:
//函數的定義在類內部的,隱式inline
void SetID(int id)
{
m_iID = id;
}
int GetID()
{
return m_iID;
}
};
一般inline函數都是放在.h文件中的,函數的聲明和定義放在一起。 如果我們在類中將一個函數的定義放在了類的內部,那麼這個函數就會被聲明為inline。當然,這一條對構造函數和析構函數也是有效的。不過,構造函數和析構函數最好不要inline的,會出現比較麻煩的情況。
4.一定會inline嗎?
答案是不一定。inline關鍵字跟其他的關鍵字不太一樣,他僅僅是一個請求,執不執行不一定。最終一個函數會不會被inline還是看編譯器的脾氣。我們寫程序的時候,在函數前面加上inline關鍵字或者直接將函數定義在類中之後,編譯時,編譯器會進行相關的分析,看一下這個函數值不值得內聯,如果不值得,它會忽略我們的inline請求。 inline函數一般對應的函數是那種體積比較小或者經常調用的函數。而這種函數的特點就是短小,函數本身的操作還沒有函數調用性能損失大。所以,對於下面兩種函數,編譯器是不會進行內聯的: (1)過長的函數。比如一個1000多行的函數,編譯器根本不會理會inline的請求的。如果這個inline了,那麼所有調用該函數的地方都插入這1000多行的程序,那麼程序的體積會膨大很多。 (2)包含循環或者遞歸的函數。 經過了這個檢查,編譯器會決定是否對函數進行inline操作。但是,這也不是絕對的,還有7種情況,編譯器仍然不會對我們的函數進行inline操作,即使是使用我們之後介紹的forceinline關鍵字,仍然不能進行inline。
三.__inline與__forceinline關鍵字
關於__inline與__forceinline關鍵字的話,之前在項目中看到過,比較糾結它們與inline的區別與聯系,今天也順便整理一下。
1.__inline關鍵字
__inline關鍵字比較簡單,它是Microsoft的編譯器中提供的一個關鍵字,可以用於C和C++但是僅限於微軟的編譯器,而Inline是標准C++提供的關鍵字,僅能用於C++,但是不限定編譯器。 而__inlined的其他情況都與inline關鍵字相同,所以這裡不再贅述。
2.__forceinline關鍵字
__forceinline關鍵字也是Microsoft的編譯器中提供的一個關鍵字,可以用於C和C++但是僅限於微軟的編譯器。 這個關鍵字看起來很牛,字面上翻譯就是強制內聯。
3.__forceinline真的能強制內聯嗎
雖然它叫強制內聯,然而能不能內聯還是要看情況滴!!顯然,它也是一個請求,最終還是編譯器決定能不能內聯。下面看一下,哪幾種情況是不能內聯的(注意,__forceinline都不能強制內聯的,inline當然就更不能內聯了): Even with __forceinline, the compiler cannot inline code in all circumstances. The compiler cannot inline a function if:
(1)The function or its caller is compiled with /Ob0 (the default option for debug builds).
(2)The function and the caller use different types of exception handling (C++ exception handling in one, structured exception handling in the other).
(3)The function has a variable argument list.
(4)The function uses inline assembly, unless compiled with /Og, /Ox, /O1, or /O2.
(5)The function is recursive and not accompanied by #pragma inline_recursion(on). With the pragma, recursive functions are inlined to a default depth of 16 calls. To reduce the inlining depth, use inline_depth pragma.
(6)The function is virtual and is called virtually. Direct calls to virtual functions can be inlined.
(7)The program takes the address of the function and the call is made via the pointer to the function. Direct calls to functions that have had their address taken can be inlined.
(8)The function is also marked with the naked __declspec modifier. 上面的內容來自微軟的MSDN,翻譯一下: (1)函數或其調用者使用/Ob0編譯器選項進行編譯(Debug模式下的默認選項)。也就是說在Debug模式下,是不會發生函數內聯的。 (2)函數和其調用者使用不同類型的異常處理。 (3)函數具有可變數目的參數。 (4)函數使用了在線匯編(即直接在你C/C++代碼裡加入匯編語言代碼)。但使用了編譯器關於優化的選項/Og,/Ox,/O1,或/O2的情況除外。 (5)函數是遞歸的並且不伴有#inline_recursion(on)。遞歸函數內聯調用默認的深度為16。為了減少內聯深度,使用inline_depth。 (6).是虛函數並且是虛調用。但對虛函數的直接調用可以inline。 (7)通過指向該函數的函數指針進行調用。 (8)函數被關鍵字__declspec(naked)修飾。
好吧,看來想要內聯一個函數還是挺難的,有這麼多要求。不過,一般的話我們只要記住這幾個關鍵點:第一,函數短小精悍,函數本身的開銷比調用函數的開銷小。第二,不要有過長的代碼,不要有循環,遞歸。第三,有virtual時不會有Inline。第四,通過函數指針調用函數的時候不會有內聯。第五,Debug下沒有內聯。
四.關於inline的一些細節問題
好吧,本人強迫症發作,決定刨根問底一下,再總結一下關於inline的幾個特殊的地方。
1.inline和virtual
首先,他倆是沖突的,但是並不會報錯。因為inline只是一個請求,在同時有virtual和inline的時候,編譯器會首先滿足virtual,而忽略我們的inline請求。其實在類中virtual函數和inline同時出現的時候還是挺多的,雖然我們可能並沒有寫inline(因為隱式Inline)。 多態的實現是由虛表加以支持的,凡是有虛函數的對象,都會在構造函數開始時構造一個虛表,虛表中的第一個元素一般是對象的類型信息,其他每個元素存放的是真正函數的地址,如果子類覆蓋了父類的虛函數,則對應的位置中的地址就會被修改,但是同一個函數在虛表中的位置即下標是相同的。當我們用基類指針或者引用調用一個虛函數時,在編譯期只知道該函數在某個虛表的第幾個位置,但是不知道是父類的虛表還是子類的虛表,只有到運行時才能確定是哪一個虛表,從而表現出多態。但如果你不是使用基類的指針或者引用調用虛函數,或者你調用的不是虛函數,則在編譯期間就可以直接找到成員函數的地址,不需要等到運行時才確定,因為此時,調用者是哪個對象已經確定,從而該函數的地址也是確定的。
雖然virtual所代表的多態類型是要在運行時確定的,但是如果調用者不是基類的指針或者引用,則該virtual的地址會在編譯期間就確定,因而此時可以用inline進行展開。即使使用了基類的指針或引用進行調用,也不會產生錯誤,此時inline將不會展開,但virtual仍然表現出多態,因為inline畢竟只是建議,而不是強制,所以兩者不矛盾。 其實簡單分析一下,就應該明白,兩者的確是沖突的。因為inline的機制是在編譯時就進行了函數的替換和展開。函數要調用什麼,這是在運行之前就決定了的。而virtual使用的是動態綁定,簡單來說就是根據運行時的動態類型,去虛函數表中查找對應的函數。所以,Inline那套編譯時替換的行為肯定是不會有多態的!!顯然,當virtual和inline沖突的時候,編譯器一定會為了正確而犧牲掉性能的。僅有我們不使用基類的指針或者引用來調用virtual函數時,Inline才會展開。
2.構造函數&析構函數不要inline
構造函數和析構函數能不能inline?答案是能。只要把函數定義在類裡面,就inline了,甭管是構造還是析構,甚至是friend。 但是構造函數和析構函數最好不要進行inline。這是《Effective C++》中的建議,不過還是來看看為什麼。 構造函數中可能有很多異常處理相關的東西,雖然我們看不到,不過這東西編譯器給加的。所以即使我們的構造函數看起來空空如也,裡面也不是真正的什麼都沒有!!加入構造函數inline了,初始化列表中的對象也是inline的話,那麼,展開之後,這個構造函數就會龐大無比。 析構函數也是如此。而且經常有徐析構函數,這個本身也是和inlne沖突的。
所以簡單粗暴的記住就好,而且VS自動生成的類也是把構造函數和析構函數分開到.cpp文件中的。
3.inline和宏的比較
沒有inline之前,貌似我們經常這樣定義一個簡單的類似函數的宏:
#define MAX(x, y) ((x) > (y) ? (x) : (y))
int _tmain(int argc, _TCHAR* argv[])
{
int a = 3;
int b = 4;
cout< 通過這樣的宏定義,達到簡單函數替換的效果。不過,這種替換跟inline相比簡直就是小巫見大巫了。首先#define就是簡單的替換,沒有什麼諸如安全類型檢查,自動類型轉化等等。而且這種替換也存在著一些風險,使用宏定義的時候要慎重得多才行。而使用inline,我們完全可以像普通函數那樣操作Inline函數,有類型檢查,自動類型轉化,而且還可以像成員函數那樣,妥善的處理this指針。並且,使用inline的話,是編譯器為我們進行更加深入的優化,這也是宏定義做不到的。編譯器還會為我們分析,是不是值得inline,如果不值得,就不會inline。4.Debug下不inline
因為inline會導致函數被拷貝到調用的地方,所以實際上Inline函數並不再是一個函數。因而絕大多數的調試器都對其束手無策。所以,在調試的時候,編譯器選擇不進行內聯處理。即,debug模式下是沒有進行內聯的。這樣,我們就可以像調試普通函數那樣調試“內聯函數”了。5.在使用函數指針調用時不Inline
雖然函數是inline函數,但是,有時候我們如果使用函數指針調用這個函數的話,那麼就不會進行inline處理,編譯器會生成一個真正的函數實體,來達到正常的函數調用過程。所以這種情況下,肯定是實體函數,函數不會被Inline。五.總結
inline是一個很好的東東,但是我們要慎用。雖然Inline可以去掉函數調用時的損失,但是這是以拷貝替換為代價的。而且將實現和聲明放在一起(不放在一起的話會導致一個inline多個實現),會導致inline函數無法隨著動態程序庫升級,如果改動inline函數,會導致所有使用該inline函數的文件被重新編譯。
正如《Effective C++》中所說,一開始不要將函數置為inline,除了那些一定為inline或者平淡無奇的函數(比如SetValue,GetValue等)。記住80-20法則,程序80%的運行時間花費在20%的代碼上。所以,我們要找出這能夠增進程序效率的20%的代碼,竭盡所能的為其瘦身以致將其inline。