正文
最近北京房?jī)r(jià)蹭蹭猛漲,買了房子的人心花怒放,沒(méi)買的人心驚肉跳,咬牙切齒,樓主作為北漂無(wú)房一族,著實(shí)又亞歷山大了一把,這些天晚上睡覺(jué)總是很難入睡,即使入睡,也是浮夢(mèng)連篇,即使亞歷山大,對(duì)C++的熱情和追求還是不減,應(yīng)該是感動(dòng)了周公吧,夢(mèng)境從此處開始,大師入場(chǎng)來(lái)給我安慰了。。。
11點(diǎn)躺在床上了,腦子里總結(jié)一下最近的工作:最近的開發(fā)用到inline函數(shù)比較多,眾所周知,inline的使用是為了提高程序性能,可結(jié)果卻總不盡如人意,這個(gè)捉急啊,嗯?怎么突然到了山腳下,周邊樹木林立,郁郁蔥蔥,鳥兒委婉啼叫,花兒盛開綻放,好愜意啊,向遠(yuǎn)處望去,青山聳入云霄,山腳下有一石門,突然發(fā)現(xiàn)旁邊坐著一位白衣人,像是在練太極,走近一看,怎么是藍(lán)眼睛,黃頭發(fā),再一定睛,我靠。。這不是傳說(shuō)中的斯考特大師么?我快步向前,用自己蹩腳的英文問(wèn)候了一句:
我:Hello,are you Scott Meyers?
大師:是的,恨高星認(rèn)識(shí)你,我認(rèn)識(shí)你,你是在博客園上又把我的書籍重新翻譯了一遍的那個(gè),你是HarlanC。你是不是有問(wèn)題要問(wèn)我呢?
我:(心理雞凍難耐,斯考特竟然會(huì)中文)Y..yes(不要結(jié)巴了),I have one question…..
大師:你還是用中文吧。
我:好吧,最近使用inline比較多,但效率卻總是不盡人意,您說(shuō)是用inline好呢還是不用好呢。
大師:跟我來(lái)。
打開山門,一個(gè)胖胖的女人站在院子里,她前面有一張桌子,桌子上面放了兩個(gè)盒子,盒子上都寫著字和標(biāo)點(diǎn)符號(hào)。一個(gè)是:巧克力?,一個(gè)是:蔬菜?大師望了我一眼,欲言又止。
我心里突然一亮,馬上回復(fù):大師,您的意思是讓胖女人猜測(cè),猜出哪個(gè)吃那個(gè)?
大師伸出食指,面帶微笑,邊搖邊說(shuō):No.No,No.胖女人代表程序,盒子里的食物代表inline后的函數(shù),你需要自己判斷這個(gè)函數(shù)是“巧克力”還是“蔬菜”,巧克力會(huì)讓胖女人的身材更加臃腫,蔬菜能夠讓胖女人瘦身。
我問(wèn)道:看您在練太極,是不是氣功能看穿盒子,教教我吧。
大師看了看我,扎下了馬步,開始運(yùn)氣了。這是要發(fā)功了吧。我心想。
運(yùn)氣完畢,大師走向盒子,用手抓住它們,用力一撕,盒子打開了,大師回頭望了我一眼,說(shuō)到:
要多動(dòng)手。
自己意淫了一把,現(xiàn)在開始進(jìn)入正題:
1. inline函數(shù)的優(yōu)缺點(diǎn)
內(nèi)聯(lián)函數(shù)——一個(gè)多么美妙的想法!它們看上去像函數(shù),行為表現(xiàn)也像函數(shù),它們總是比宏要優(yōu)秀許多(Item 2),你能調(diào)用它們卻沒(méi)有引入函數(shù)調(diào)用的開銷,行為表現(xiàn)如此,夫復(fù)何求?
你實(shí)際上比你想象的要獲取的更多,因?yàn)楸苊夂瘮?shù)調(diào)用的開銷只是這個(gè)故事的一部分。編譯器最優(yōu)化是為了濃縮沒(méi)有函數(shù)調(diào)用的代碼而設(shè)計(jì),所以當(dāng)你inline一個(gè)函數(shù)時(shí),你可能使編譯器在函數(shù)體上執(zhí)行特定場(chǎng)景下的優(yōu)化操作。大多數(shù)編譯器不會(huì)在outlined的函數(shù)調(diào)用上執(zhí)行這樣的優(yōu)化。
然而編程猶如生活,沒(méi)有免費(fèi)的午餐,inline函數(shù)也不例外。Inline函數(shù)的內(nèi)部機(jī)制是用函數(shù)體替換函數(shù)調(diào)用,即使沒(méi)有統(tǒng)計(jì)學(xué)的博士學(xué)位你也能看到這似乎增加了你的目標(biāo)碼的大小。在內(nèi)存有限的機(jī)器上,過(guò)度的inlining會(huì)造成占用空間過(guò)大的問(wèn)題。即使使用虛擬內(nèi)存,inline代碼造成的膨脹也會(huì)導(dǎo)致額外的分頁(yè),指令緩存命中率降低以及隨之而來(lái)的性能損耗。
另一方面,如果inline函數(shù)體非常短小,函數(shù)體本身生成的代碼可能比函數(shù)調(diào)用生成的代碼體積要小。如果是這種情況,inlining函數(shù)使目標(biāo)代碼體積更小以及指令緩存命中率更高!
2. Inline函數(shù)的顯示和隱式實(shí)現(xiàn)方式
需要注意的是inline是對(duì)編譯器的請(qǐng)求而不是命令。請(qǐng)求可以顯示或者隱式的提出來(lái)。隱式的方法通過(guò)在類定義內(nèi)定義一個(gè)函數(shù):
1 class Person {2 public:3 ...4 int age() const { return theAge; } // an implicit inline request: age is5 ... // defined in a class definition6 private:7 int theAge;8 };
這樣的函數(shù)通常是成員函數(shù),但是Item 46中解釋道friend函數(shù)也能在類中定義。如果是這樣,它們也會(huì)被隱式聲明成inline。
顯示的聲明一個(gè)inline函數(shù)的方法是在函數(shù)定義之前加上關(guān)鍵字inline。舉個(gè)例子,下面是標(biāo)準(zhǔn)的max模板實(shí)現(xiàn)方式:
1 template<typename T> // an explicit inline2 inline const T& std::max(const T& a, const T& b) // request: std::max is3 { return a < b ? b : a; } // preceded by “inline”
3. 函數(shù)模板必須inline么?
max被實(shí)現(xiàn)為模板函數(shù)的事實(shí)讓我們聯(lián)想到inline函數(shù)和模板都是需要被定義在頭文件中的。因此一些程序要就下結(jié)論函數(shù)模板就必須是inline的。這個(gè)結(jié)論既無(wú)效并可能會(huì)有潛在的危害,讓我們分析分析吧。
Inline函數(shù)是必須被定義在頭文件中的,因?yàn)榇蠖鄶?shù)編譯環(huán)境在編譯時(shí)執(zhí)行函數(shù)的內(nèi)聯(lián)。為了將函數(shù)調(diào)用替換為函數(shù)體,編譯器必須了解這個(gè)函數(shù)長(zhǎng)成什么樣子。(一些編譯環(huán)境能夠在鏈接的時(shí)候執(zhí)行內(nèi)聯(lián),甚至有一些能夠在運(yùn)行時(shí)進(jìn)行內(nèi)聯(lián)(如基于.NET CLI的托管環(huán)境),這樣的環(huán)境都是例外,但不是通用規(guī)則。在大多數(shù)C++程序中inline是編譯時(shí)活動(dòng)。)
模板也是被定義在頭文件中的,因?yàn)榫幾g器為了對(duì)其進(jìn)行實(shí)例化時(shí)需要知道這個(gè)模板是什么樣子的。(這種情況也有例外,一些編譯環(huán)境在鏈接期間執(zhí)行模板實(shí)例化。然而編譯時(shí)實(shí)例化是最常見的。)
模板實(shí)例化和inline是相互獨(dú)立的。如果你實(shí)現(xiàn)一個(gè)函數(shù)模板,而需要此模版實(shí)例化的所有函數(shù)都是inline的,那么將其聲明成inline。上面的std::max就是這么實(shí)現(xiàn)的。但如果你將函數(shù)實(shí)現(xiàn)成模板,而此函數(shù)不需要inline,那么避免將模板聲明成inline(無(wú)論是顯示的還是隱式的)。使用inline是有代價(jià)的,不要在沒(méi)有進(jìn)行考慮周詳之前使用inline。我們已經(jīng)提及了inline是如何導(dǎo)致代碼膨脹的(Item 44中為模板作者描述了一個(gè)特別重要的注意點(diǎn)),但也會(huì)有其他的開銷,我們一會(huì)討論。
4. 深入理解inline
在我們進(jìn)行討論之前,先讓我們了解如下事實(shí):inline只是一個(gè)對(duì)編譯器的請(qǐng)求,而編譯器可能會(huì)將其忽略。大多數(shù)編譯器會(huì)拒絕為看上去特別復(fù)雜的函數(shù)進(jìn)行inline(例如,包含循環(huán)或者迭代的函數(shù)),需要調(diào)用虛函數(shù)的函數(shù)也不能進(jìn)行inline,不要感到吃驚。virtual意味著“只有在運(yùn)行時(shí)才能決定調(diào)用哪個(gè)函數(shù),”而inline意味著“執(zhí)行程序之前,在調(diào)用點(diǎn)處用函數(shù)體進(jìn)行替換”。如果編譯器不知道將會(huì)調(diào)用哪個(gè)函數(shù),你就不能因?yàn)榫芙^為函數(shù)體內(nèi)聯(lián)而責(zé)備它。
我們總結(jié)一下:一個(gè)定義成inline的函數(shù)是否真正被inline取決于你所使用的編譯環(huán)境——而這個(gè)編譯環(huán)境主要是只編譯器。幸運(yùn)的是,編譯器會(huì)對(duì)這個(gè)過(guò)程進(jìn)行診斷,如果inline一個(gè)函數(shù)失敗了,它會(huì)發(fā)出一個(gè)警告(Item 53)。
有時(shí)候即使編譯器迫切的希望對(duì)函數(shù)進(jìn)行inline,它們也會(huì)為其生成一個(gè)單獨(dú)的函數(shù)體。例如,如果你的程序需要獲知內(nèi)聯(lián)函數(shù)的地址,編譯器就必須為其生成一個(gè)outline的函數(shù)體。它們不能使用一個(gè)不存在的函數(shù)指針吧?加上如下事實(shí):編譯器使用函數(shù)指針進(jìn)行函數(shù)調(diào)用時(shí)不會(huì)為其進(jìn)行inline,這意味著對(duì)內(nèi)聯(lián)函數(shù)的調(diào)用可能會(huì)被內(nèi)聯(lián)也可能不會(huì),取決于函數(shù)調(diào)用是如何進(jìn)行的:
1 inline void f() {...} // assume compilers are willing to inline calls to f2 3 void (*pf )() = f; // pf points to f4 5 ...6 f(); // this call will be inlined, because it’s a “normal” call7 8 pf(); // this call probably won’t be, because it’s through9 // a function pointer
未被inline的inline函數(shù)會(huì)像幽靈一樣縈繞在你周圍,即使你從未使用函數(shù)指針也是如此,因?yàn)椴⒉皇侵挥谐绦騿T才會(huì)需要函數(shù)指針。有時(shí)候編譯器也會(huì)為構(gòu)造函數(shù)和析構(gòu)函數(shù)生成一份out-of-line函數(shù)體,因?yàn)閷?duì)數(shù)組中的對(duì)象進(jìn)行構(gòu)造和析構(gòu)時(shí)需要使用指向它們的指針。
5. 構(gòu)造函數(shù)和析構(gòu)函數(shù)該不該被inline?
事實(shí)上,構(gòu)造函數(shù)和析構(gòu)函數(shù)通常情況下是inline函數(shù)的槽糕候選人,而不像表面看上去那樣,考慮類Derived類的構(gòu)造函數(shù):
1 class Base { 2 public: 3 ... 4 private: 5 6 std::string bm1, bm2; // base members 1 and 2 7 8 }; 9 10 class Derived: public Base { 11 12 public: 13 14 Derived() {} // Derived’s ctor is empty — or is it?15 16 ... 17 18 private: 19 20 21 22 std::string dm1, dm2, dm3; // derived members 1–323 24 };
這個(gè)構(gòu)造函數(shù)看上去像是inline函數(shù)的杰出候選人,因?yàn)樗话魏未a。但是不要被表面現(xiàn)象蒙蔽。
當(dāng)對(duì)象被創(chuàng)建或者析構(gòu)的時(shí)候C++必須保證一些事情的發(fā)生。例如,當(dāng)你使用new的時(shí)候,你的動(dòng)態(tài)創(chuàng)建的對(duì)象由它們的構(gòu)造函數(shù)自動(dòng)初始化;當(dāng)你使用delete時(shí),對(duì)應(yīng)的析構(gòu)函數(shù)要被觸發(fā)。當(dāng)你創(chuàng)建一個(gè)對(duì)象時(shí),對(duì)象的基類部分和它的每個(gè)數(shù)據(jù)成員都會(huì)被自動(dòng)構(gòu)建,當(dāng)對(duì)象被銷毀的時(shí)候相反的過(guò)程也就是自動(dòng)析構(gòu)就會(huì)發(fā)生。如果在構(gòu)造或者析構(gòu)的時(shí)候拋出異常,已經(jīng)被構(gòu)建出來(lái)的對(duì)象的任何部分都應(yīng)該被自動(dòng)釋放。在所有這些場(chǎng)景中,c++指出什么必須發(fā)生,但沒(méi)說(shuō)明如何發(fā)生。這就是編譯器實(shí)現(xiàn)人員要做的了,但是應(yīng)該清楚的是這些事情是不會(huì)自己發(fā)生的。你必須在你的程序中寫一些代碼來(lái)讓這些事情發(fā)生,這些在編譯過(guò)程中一定會(huì)插入到你的代碼的某些地方。有時(shí)候在構(gòu)造函數(shù)和析構(gòu)函數(shù)的結(jié)尾處,所以我們可以想象一個(gè)空的Derived 構(gòu)造函數(shù)實(shí)際上會(huì)是什么樣子的:
1 Derived::Derived() // conceptual implementation of 2 { // “empty” Derived ctor 3 4 Base::Base(); // initialize Base part 5 6 try { dm1.std::string::string(); } // try to construct dm1 7 8 9 catch (...) { // if it throws,10 Base::~Base(); // destroy base class part and11 throw; // propagate the exception12 }13 try { dm2.std::string::string(); } // try to construct dm214 catch(...) { // if it throws,15 dm1.std::string::~string(); // destroy dm1,16 Base::~Base(); // destroy base class part, and17 18 throw; // propagate the exception19 20 } 21 22 23 try { dm3.std::string::string(); } // construct dm324 catch(...) { // if it throws,25 dm2.std::string::~string(); // destroy dm2,26 dm1.std::string::~string(); // destroy dm1,27 Base::~Base(); // destroy base class part, and28 throw; // propagate the exception29 }30 }
這么寫并不代表著編譯器一定會(huì)這么做,因?yàn)榫幾g器處理異常的方式更加復(fù)雜。但是這精確的反映出Derived的空構(gòu)造函數(shù)必須提供什么。不管編譯器對(duì)異常處理的實(shí)現(xiàn)多么復(fù)雜,Derived的構(gòu)造函數(shù)必須為其數(shù)據(jù)成員和基類調(diào)用構(gòu)造函數(shù),這些調(diào)用(可能它們本身是inline的)會(huì)影響inline的吸引力。
同樣的原因適用于基類構(gòu)造函數(shù),因此如果它被inline了,它里面的代碼同樣會(huì)被插入到Derived構(gòu)造函數(shù)中(Derived構(gòu)造函數(shù)會(huì)調(diào)用基類構(gòu)造函數(shù)。)。并且如果string構(gòu)造函數(shù)恰恰也被inline了,Derived構(gòu)造函數(shù)會(huì)增加5份函數(shù)代碼的拷貝(對(duì)應(yīng)Derived中的5個(gè)string),現(xiàn)在你應(yīng)該明白了為什么對(duì)Derived構(gòu)造函數(shù)進(jìn)行inline是一個(gè)沒(méi)腦子的決定。同樣的考慮也適用于Derived析構(gòu)函數(shù),我們必須看到被Derived構(gòu)造函數(shù)初始化的對(duì)象被合適的銷毀掉。
6. Inline對(duì)客戶造成的影響
庫(kù)設(shè)計(jì)者必須估計(jì)將函數(shù)聲明成inline會(huì)造成的影響,因?yàn)樵谝粋€(gè)庫(kù)中為客戶可見的inline函數(shù)提供二進(jìn)制更新(binary upgrade)是不可能的。用其他的話來(lái)說(shuō),如果f是一個(gè)庫(kù)中的inline函數(shù),這個(gè)庫(kù)的客戶將f這個(gè)函數(shù)體編譯進(jìn)了自己的應(yīng)用中。如果庫(kù)實(shí)現(xiàn)者過(guò)后決定修改f,所有使用f的客戶都必須重新編譯。這是不受歡迎的做法。另外一方面,如果f不是inline函數(shù),對(duì)f的修改只需要重新鏈接就可以了。這實(shí)際上比重新編譯減少了負(fù)擔(dān),如果包含這個(gè)函數(shù)的庫(kù)是被動(dòng)態(tài)鏈接的,更新版本會(huì)被客戶不知不覺(jué)的吸收。
7. Inline對(duì)調(diào)試器(debugger)產(chǎn)生的影響
為了更好的開發(fā)程序,將上面的考慮都記在腦海中,但在編碼過(guò)程中從實(shí)用的角度來(lái)說(shuō),一個(gè)事實(shí)支配了其他所有問(wèn)題:大多數(shù)調(diào)試器不能很好的應(yīng)用在inline函數(shù)上。這應(yīng)該也不是什么出乎意料的事。你如何才能在一個(gè)并沒(méi)有那里的函數(shù)設(shè)定斷點(diǎn)呢?雖然一些編譯環(huán)境支持對(duì)inline函數(shù)的調(diào)試,許多編譯環(huán)境只是在生成調(diào)試版本的時(shí)候禁止inline。
8. 總結(jié)
決定哪些函數(shù)應(yīng)該被聲明為inline的哪些不應(yīng)該是一個(gè)邏輯策略問(wèn)題。首先,不要inline任何東西,或者只將inline限定在那些必須被inline的函數(shù)(Item 46)或者很小的函數(shù)上面。通過(guò)謹(jǐn)慎的使用inline,你能很好的使用你的調(diào)試器,但是這樣做你也同樣將inline放在了合適的位置:作為手工優(yōu)化的方法。不要忘記80-20法則,這意味著一個(gè)特定的程序會(huì)用80%的時(shí)間來(lái)執(zhí)行20%的代碼。這是個(gè)重要法則,因?yàn)樗嵝蚜四悖鳛橐粋€(gè)軟件工程師識(shí)別這20%的代碼并進(jìn)行優(yōu)化會(huì)對(duì)程序的性能有整體的提升。你可以對(duì)你的函數(shù)進(jìn)行inline或者去掉inline,直到性能滿足要求,當(dāng)然這需要你在那20%的函數(shù)上努力,否則就是浪費(fèi)精力。
需要你記住的:
將inline限定在最小的,最頻繁調(diào)用的函數(shù)上面。這會(huì)使你的調(diào)試,二進(jìn)制升級(jí)變得容易,并能將潛在的代碼膨脹問(wèn)題最小化,提高程序運(yùn)行速度可能性最大化。
不要僅僅因?yàn)楹瘮?shù)模板出現(xiàn)在頭文件中就將其聲明成內(nèi)聯(lián)函數(shù)。
作者: HarlanC
博客地址: http://www.cnblogs.com/harlanc/
個(gè)人博客: http://www.harlancn.me/
本文版權(quán)歸作者和博客園共有,歡迎轉(zhuǎn)載,但未經(jīng)作者同意必須保留此段聲明,且在文章頁(yè)面明顯位置給出, 原文鏈接 .
http://www.cnblogs.com/harlanc/p/6523201.html