明輝手游網(wǎng)中心:是一個(gè)免費(fèi)提供流行視頻軟件教程、在線學(xué)習(xí)分享的學(xué)習(xí)平臺(tái)!

C++箴言:理解inline化的介入與排除

[摘要]inline 函數(shù)——多么棒的主意!它們看起來(lái)像函數(shù),它們產(chǎn)生的效果也像函數(shù),它們?cè)诟鞣矫娑急群旰玫锰嗵啵銋s可以在調(diào)用它們時(shí)不招致函數(shù)調(diào)用的成本。你還有什么更多的要求呢?   實(shí)際上你得到的可能比你想的更多,因?yàn)楸苊夂瘮?shù)調(diào)用的成本只是故事的一部分。在典型情況下,編譯器的優(yōu)化是為了一段連續(xù)...
inline 函數(shù)——多么棒的主意!它們看起來(lái)像函數(shù),它們產(chǎn)生的效果也像函數(shù),它們?cè)诟鞣矫娑急群旰玫锰嗵,而你卻可以在調(diào)用它們時(shí)不招致函數(shù)調(diào)用的成本。你還有什么更多的要求呢?

  實(shí)際上你得到的可能比你想的更多,因?yàn)楸苊夂瘮?shù)調(diào)用的成本只是故事的一部分。在典型情況下,編譯器的優(yōu)化是為了一段連續(xù)的沒(méi)有函數(shù)調(diào)用的代碼設(shè)計(jì)的,所以當(dāng)你 inline 化一個(gè)函數(shù),你可能就使得編譯器能夠?qū)瘮?shù)體實(shí)行上下文相關(guān)的特殊優(yōu)化。大多數(shù)編譯器都不會(huì)對(duì) "outlined" 函數(shù)調(diào)用實(shí)行這樣的優(yōu)化。

  然而,在編程中,就像在生活中,沒(méi)有免費(fèi)午餐,而 inline 函數(shù)也不例外。一個(gè) inline 函數(shù)背后的思想是用函數(shù)本體代替每一處對(duì)這個(gè)函數(shù)的調(diào)用,而且不必拿著統(tǒng)計(jì)表中的 Ph.D. 就可以看出這樣可能會(huì)增加你的目標(biāo)代碼的大小。在有限內(nèi)存的機(jī)器上,過(guò)分熱衷于 inline 化會(huì)使得程序?qū)τ诳捎每臻g來(lái)說(shuō)過(guò)于龐大。即使使用了虛擬內(nèi)存,inline 引起的代碼膨脹也會(huì)導(dǎo)致附加的分頁(yè)調(diào)度,減少指令緩存命中率,以及隨之而來(lái)的性能損失。

  在另一方面,如果一個(gè) inline 函數(shù)本體很短,為函數(shù)本體生成的代碼可能比為一個(gè)函數(shù)調(diào)用生成的代碼還要小。如果是這種情況,inline 化這個(gè)函數(shù)可以實(shí)際上導(dǎo)致更小的目標(biāo)代碼和更高的指令緩存命中率! 記住,inline 是向編譯器發(fā)出的一個(gè)請(qǐng)求,而不是一個(gè)命令。這個(gè)請(qǐng)求能夠以顯式的或隱式的方式提出。隱式的方法就是在一個(gè)類定義的內(nèi)部定義一個(gè)函數(shù):

class Person {
 public:
  ...
  int age() const { return theAge; } // an implicit inline request: age is
  ... // defined in a class definition

 private:
  int theAge;
};

  這樣的函數(shù)通常是成員函數(shù),不過(guò)我們知道友元函數(shù)也能被定義在類的內(nèi)部,如果它們?cè)谀抢,它們也被隱式地聲明為 inline。

  顯式的聲明一個(gè) inline 函數(shù)的方法是在它的聲明之前加上 inline 關(guān)鍵字。例如,以下就是標(biāo)準(zhǔn) max 模板(來(lái)自 <algorithm>)經(jīng)常用到的的實(shí)現(xiàn)方法:

template<typename T> // an explicit inline
inline const T& std::max(const T& a, const T& b) // request: std::max is
{ return a < b ? b : a; } // preceded by "inline"

  max 是一個(gè)模板的事實(shí)引出一個(gè)觀察結(jié)論:inline 函數(shù)和模板一般都是定義在頭文件中的。這就使得一些程序員得出結(jié)論斷定函數(shù)模板必須是 inline。這個(gè)結(jié)論是非法的而且有潛在的危害,所以它值得我們考察一下。 inline 函數(shù)一般必須在頭文件內(nèi),因?yàn)榇蠖鄶?shù)構(gòu)建環(huán)境在編譯期間進(jìn)行 inline 化。為了用被調(diào)用函數(shù)的函數(shù)本體替換一個(gè)函數(shù)調(diào)用,編譯器必須知道函數(shù)看起來(lái)像什么樣子。(有一些構(gòu)建環(huán)境可以在連接期間進(jìn)行 inline 化,還有少數(shù)幾個(gè)——比如,基于 .NET Common Language Infrastructure (CLI) 的控制環(huán)境——居然能在運(yùn)行時(shí) inline 化。然而,這些環(huán)境都是例外,并非規(guī)則。inline 化在大多數(shù) C++ 程序中是一個(gè)編譯時(shí)行為。)

  模板一般在頭文件內(nèi),因?yàn)榫幾g器需要知道一個(gè)模板看起來(lái)像什么以便用到它時(shí)對(duì)它進(jìn)行實(shí)例化。(同樣,也不是全部如此。一些構(gòu)建環(huán)境可以在連接期間進(jìn)行模板實(shí)例化。然而,編譯期實(shí)例化更為普遍。) 模板實(shí)例化與 inline 化無(wú)關(guān)。如果你寫了一個(gè)模板,而且你認(rèn)為所有從這個(gè)模板實(shí)例化出來(lái)的函數(shù)都應(yīng)該是 inline 的,那么就聲明這個(gè)模板為 inline,這就是上面的 std::max 的實(shí)現(xiàn)被做的事情。但是如果你為沒(méi)有理由要 inline 化的函數(shù)寫了一個(gè)模板,就要避免聲明這個(gè)模板為 inline(無(wú)論顯式的還是隱式的)。inline 化是有成本的,而且你不希望在毫無(wú)預(yù)見(jiàn)的情況下遭遇它們。我們已經(jīng)說(shuō)到 inline 化是如何引起代碼膨脹的,但是,還有其它的成本,過(guò)一會(huì)兒我們?cè)儆懻摗?br>
  在做這件事之前,我們先來(lái)完成對(duì)這個(gè)結(jié)論的考察:inline 是一個(gè)編譯器可能忽略的請(qǐng)求。大多數(shù)編譯器拒絕它們認(rèn)為太復(fù)雜的 inline 函數(shù)(例如,那些包含循環(huán)或者遞歸的),而且,除了最細(xì)碎的以外的全部虛擬函數(shù)的調(diào)用都不會(huì)被 inline 化。不應(yīng)該對(duì)這后一個(gè)結(jié)論感到驚訝。虛擬意味著“等待,直到運(yùn)行時(shí)才能斷定哪一個(gè)函數(shù)被調(diào)用”,而 inline 意味著“執(zhí)行之前,用被調(diào)用函數(shù)取代調(diào)用的地方”。如果編譯器不知道哪一個(gè)函數(shù)將被調(diào)用,你很難責(zé)備它們拒絕 inline 化這個(gè)函數(shù)本體。

  所有這些加在一起,得出:一個(gè)被指定的 inline 函數(shù)是否能真的被 inline 化,取決于你所使用的構(gòu)建環(huán)境——主要是編譯器。幸運(yùn)的是,大多數(shù)編譯器都有一個(gè)診斷層次,在它們不能 inline 化一個(gè)你提出的函數(shù)時(shí),會(huì)導(dǎo)致一個(gè)警告。

  有時(shí)候,即使當(dāng)編譯器完全心甘情愿地 inline 化一個(gè)函數(shù),他們還是會(huì)為這個(gè) inline 函數(shù)生成函數(shù)本體。例如,如果你的程序要持有一個(gè) inline 函數(shù)的地址,編譯器必須為它生成一個(gè) outlined 函數(shù)本體。他們?cè)趺茨苌梢粋(gè)指向根本不存在的函數(shù)的指針呢?再加上,編譯器一般不會(huì)對(duì)通過(guò)函數(shù)指針的調(diào)用進(jìn)行 inline 化,這就意味著,對(duì)一個(gè) inline 函數(shù)的調(diào)用可能被也可能不被 inline 化,依賴于這個(gè)調(diào)用是如何做成的:

inline void f() {...} // assume compilers are willing to inline calls to f

void (*pf)() = f; // pf points to f
...

f(); // this call will be inlined, because it’s a "normal" call
pf(); // this call probably won’t be, because it’s through
// a function pointer

  甚至在你從來(lái)沒(méi)有使用函數(shù)指針的時(shí)候,未 inline 化的 inline 函數(shù)的幽靈也會(huì)時(shí)不時(shí)地拜訪你,因?yàn)槌绦騿T并不必然是函數(shù)指針的唯一需求者。有時(shí)候編譯器會(huì)生成構(gòu)造函數(shù)和析構(gòu)函數(shù)的 out-of-line 拷貝,以便它們能得到指向這些函數(shù)的指針,在對(duì)數(shù)組中的對(duì)象進(jìn)行構(gòu)造和析構(gòu)時(shí)使用。

  事實(shí)上,構(gòu)造函數(shù)和析構(gòu)函數(shù)對(duì)于 inline 化來(lái)說(shuō)經(jīng)常是一個(gè)比你在不經(jīng)意的檢查中所能顯示出來(lái)的更加糟糕的候選者。例如,考慮下面這個(gè)類 Derived 的構(gòu)造函數(shù):

class Base {
 public:
  ...

 private:
  std::string bm1, bm2; // base members 1 and 2
};

class Derived: public Base {
 public:
  Derived() {} // Derived’s ctor is empty - or is it?
  ...

 private:
  std::string dm1, dm2, dm3; // derived members 1-3
};

  這個(gè)構(gòu)造函數(shù)看上去像一個(gè) inline 化的極好的候選者,因?yàn)樗话a。但是視覺(jué)會(huì)被欺騙。

  C++ 為對(duì)象被創(chuàng)建和被銷毀時(shí)所發(fā)生的事情做出了各種保證。例如,當(dāng)你使用 new 時(shí),你的動(dòng)態(tài)的被創(chuàng)建對(duì)象會(huì)被它們的構(gòu)造函數(shù)自動(dòng)初始化,而當(dāng)你使用 delete。則相應(yīng)的析構(gòu)函數(shù)會(huì)被調(diào)用。當(dāng)你創(chuàng)建一個(gè)對(duì)象時(shí),這個(gè)對(duì)象的每一個(gè)基類和每一個(gè)數(shù)據(jù)成員都會(huì)自動(dòng)構(gòu)造,而當(dāng)一個(gè)對(duì)象被銷毀時(shí),則發(fā)生關(guān)于析構(gòu)的反向過(guò)程。如果在一個(gè)對(duì)象構(gòu)造期間有一個(gè)異常被拋出,這個(gè)對(duì)象已經(jīng)完成構(gòu)造的任何部分都被自動(dòng)銷毀。所有這些情節(jié),C++ 只說(shuō)什么必須發(fā)生,但沒(méi)有說(shuō)如何發(fā)生。那是編譯器的實(shí)現(xiàn)者的事,但顯然這些事情不會(huì)自己發(fā)生。在你的程序中必須有一些代碼使這些事發(fā)生,而這些代碼——由編譯器寫出的代碼和在編譯期間插入你的程序的代碼——必須位于某處。有時(shí)它們最終就位于構(gòu)造函數(shù)和析構(gòu)函數(shù)中,所以我們可以設(shè)想實(shí)現(xiàn)為上面那個(gè)聲稱為空的 Derived 的構(gòu)造函數(shù)生成的代碼就相當(dāng)于下面這樣:

Derived::Derived() // conceptual implementation of
{
 // "empty" Derived ctor

 Base::Base(); // initialize Base part

 try { dm1.std::string::string(); } // try to construct dm1
 catch (...) { // if it throws,
  Base::~Base(); // destroy base class part and
 throw; // propagate the exception
}

try { dm2.std::string::string(); } // try to construct dm2
catch(...) {
 // if it throws,
 dm1.std::string::~string(); // destroy dm1,
 Base::~Base(); // destroy base class part, and
throw; // propagate the exception
}

try { dm3.std::string::string(); } // construct dm3
catch(...) {
 // if it throws,
 dm2.std::string::~string(); // destroy dm2,
 dm1.std::string::~string(); // destroy dm1,
 Base::~Base(); // destroy base class part, and
throw; // propagate the exception
}
}

  這些代碼并不代表真正的編譯器所生成的,因?yàn)檎嬲木幾g器會(huì)用更復(fù)雜的方法處理異常。盡管如此,它還是準(zhǔn)確地反映了 Derived 的“空”構(gòu)造函數(shù)必須提供的行為。不論一個(gè)編譯器的異常多么復(fù)雜,Derived 的構(gòu)造函數(shù)至少必須調(diào)用它的數(shù)據(jù)成員和基類的構(gòu)造函數(shù),而這些調(diào)用(它們自己也可能是 inline 的)會(huì)影響它對(duì)于 inline 化的吸引力。

  同樣的原因也適用于 Base 的構(gòu)造函數(shù),所以如果它是 inline 的,插入它的全部代碼也要插入 Derived 的構(gòu)造函數(shù)(通過(guò) Derived 的構(gòu)造函數(shù)對(duì) Base 的構(gòu)造函數(shù)的調(diào)用)。而且如果 string 的構(gòu)造函數(shù)碰巧也是 inline 的,Derived 的構(gòu)造函數(shù)中將增加五個(gè)那個(gè)函數(shù)代碼的拷貝,分別對(duì)應(yīng)于 Derived 對(duì)象中的五個(gè) strings(兩個(gè)繼承的加上三個(gè)它自己聲明的)。也許在現(xiàn)在,為什么說(shuō)是否 inline 化 Derived 的構(gòu)造函數(shù)不是一個(gè)不經(jīng)大腦的決定就很清楚了。類似的考慮也適用于 Derived 的析構(gòu)函數(shù),用同樣的或者不同的方法,必須保證所有被 Derived 的構(gòu)造函數(shù)初始化的對(duì)象被完全銷毀。

  庫(kù)設(shè)計(jì)者必須評(píng)估聲明函數(shù)為 inline 的影響,因?yàn)闉閹?kù)中的客戶可見(jiàn)的 inline 函數(shù)提供二進(jìn)制升級(jí)版本是不可能的。換句話說(shuō),如果 f 是一個(gè)庫(kù)中的一個(gè) inline 函數(shù),庫(kù)的客戶將函數(shù) f 的本體編譯到他們的應(yīng)用程序中。如果一個(gè)庫(kù)的實(shí)現(xiàn)者后來(lái)決定修改 f,所有使用了 f 的客戶都必須重新編譯。這常常會(huì)令人厭煩。在另一方面,如果 f 是一個(gè)非 inline 函數(shù),對(duì) f 的改變只需要客戶重新連接。這與重新編譯相比顯然減輕了很大的負(fù)擔(dān),而且,如果庫(kù)中包含的函數(shù)是動(dòng)態(tài)鏈接的,這就是一種對(duì)于用戶來(lái)說(shuō)完全透明的方法。
 
  為了程序開(kāi)發(fā)的目標(biāo),在頭腦中牢記這些需要考慮的事項(xiàng)是很重要的,但是從編碼期間的實(shí)用觀點(diǎn)來(lái)看,占有支配地位的事實(shí)是:大多數(shù)調(diào)試器會(huì)與 inline 函數(shù)發(fā)生沖突。這不應(yīng)該是什么重大的發(fā)現(xiàn)。你怎么能在一個(gè)不在那里的函數(shù)中設(shè)置斷點(diǎn)呢?雖然一些構(gòu)建環(huán)境設(shè)法支持 inline 函數(shù)的調(diào)試,多數(shù)環(huán)境還是簡(jiǎn)單地為調(diào)試構(gòu)建取消了 inline 化。

  這就導(dǎo)出了一個(gè)用于決定哪些函數(shù)應(yīng)該被聲明為 inline,哪些不應(yīng)該的合乎邏輯的策略。最初,不要 inline 任何東西,或者至少要將你的 inline 化的范圍限制在那些必須 inline 的和那些實(shí)在微不足道的函數(shù)上。通過(guò)慎重地使用 inline,你可以使調(diào)試器的使用變得容易,但是你也將 inline 化放在了它本來(lái)應(yīng)該在的地位:作為一種手動(dòng)的優(yōu)化。不要忘記由經(jīng)驗(yàn)確定的 80-20 規(guī)則,它宣稱一個(gè)典型的程序用 80% 的時(shí)間執(zhí)行 20% 的代碼。這是一個(gè)重要的規(guī)則,因?yàn)樗嵝涯阕鳛橐粋(gè)軟件開(kāi)發(fā)者的目標(biāo)是識(shí)別出能全面提升你的程序性能的 20% 的代碼。你可以 inline 或者用其他方式無(wú)限期地調(diào)節(jié)你的函數(shù),但除非你將精力集中在正確的函數(shù)上,否則就是白白浪費(fèi)精力。

  Things to Remember

  ·將大部分 inline 限制在小的,調(diào)用頻繁的函數(shù)上。這使得程序調(diào)試和二進(jìn)制升級(jí)更加容易,最小化潛在的代碼膨脹,并最大化提高程序速度的幾率。

  ·不要僅僅因?yàn)楹瘮?shù)模板出現(xiàn)在頭文件中,就將它聲明為 inline。