• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>

            洛譯小筑

            別來無恙,我的老友…
            隨筆 - 45, 文章 - 0, 評論 - 172, 引用 - 0
            數據加載中……

            [ECPP讀書筆記 條目30] 深入探究內聯函數

            內聯函數——多么振奮人心的一項發明!它們看上去與函數很相像,它們擁有與函數類似的行為,它們要比宏(參見條目2)好用的多,同時你在調用它們時帶來的開銷比一般函數小得多。可謂“內聯在手,別無他求。”

            你得到的遠遠比你想象的要多,因為節約函數調用的開銷僅僅是冰山一角。我們知道編譯器優化通常是針對那些沒有函數調用的代碼,因此當你編寫內聯函數時,編譯器就會針對函數體的上下文進行優化工作。大多數編譯器都不會針對“外聯”函數調用進行這樣的優化。

            然而,在你的編程生涯中,“沒有免費的午餐”這句生活哲言同樣奏效,內聯函數也沒有例外。內聯函數背后蘊含的理念是:用代碼本體來取代每次函數調用,這樣做很可能會使目標代碼的體積增大不少,這一點并不是非要統計學博士才能看得清。對于內存空間有限的機器而言,過分熱衷于使用內聯則會造成過多的空間占用。即使在虛擬內存中,那些冗余的內聯代碼也會帶來不少無謂的分頁,從而使緩存讀取命中率降低,最終帶來性能的犧牲。

            另一方面,如果一個內聯函數體非常的短,那么為函數體所生成代碼的體積則可能會比調用函數所生成的代碼小一些。此時,內聯函數才真正做到了減小目標代碼和提高緩存讀取命中率的目的。

            請時刻牢記,Inline是對編譯器的一次請求,而不是一條命令。這種請求可以顯式提出也可以隱式提出。隱式請求的途徑就是:在類定義的內部定義函數:

            class Person {

            public:

              ...

              int age() const { return theAge; }    // 隱式內聯請求:

              ...                                   // 年齡age在類定義中做出定義

             

            private:

              int theAge;

            };

            這樣的函數通常是成員函數,但是類中定義的函數也可以是友元(參見條目46),如果函數是友元,那么它們會被隱式定義為內聯函數。

            顯式聲明內聯函數的方法為:在函數定義之前添加inline關鍵字。比如說,下面是標準max模板(來自<algorithm>)常見的定義方式:

            template<typename T>               // 顯式內聯請求:

            inline const T& std::max(const T& a, const T& b)

            { return a < b ? b : a; }         // std::max的前邊添加”inline”

            max是模板”這一事實,讓我們不免得出這樣的推論:內聯函數和模板都應該定義在頭文件中。這就使一些程序員做出“函數模板必須是內聯函數”的論斷。這一結論不僅站不住腳,而且也存在潛在的害處,所以這里還是要稍稍深入了解一下。

            由于大多數編程環境的內聯操作都是在編譯過程中進行的,因此內聯函數一般都應該定義在頭文件中。編譯器必須首先了解函數的情況,以便于用所調用函數體來代替這次函數調用。(一些構建環境在連接過程中進行內聯,還有個別基于.NET通用語言基礎結構(CLI)的環境甚至是在運行時進行內聯。這樣的環境屬于例外,不是守則。在大多數C++程序中,內聯是一個編譯時行為。)

            模板通常保存在頭文件中,這是因為編譯器需要了解這些模板,以便于在使用時進行正確的實例化。(再次說明,這并不是一成不變的。一些編程環境在連接時進行模板實例化。但是編譯時實例化是更通用的方式。)

            模板實例化相對于內聯是獨立的。如果你正在編寫一個模板,而你又確信由這個模板所實例化出的所有函數都應該是內聯的,那么這個模板就應該添加inline關鍵字;這也就是上文中std::max實現的做法。但是如果你正在編寫的模板并不需要內聯函數,那么就不要聲明內聯模板(無論是顯式還是隱式)。內聯也是有開銷的,不計成本的引入內聯并不明智。我們已經介紹過內聯是如何使代碼膨脹起來的(對于模板的作者而言,還應該做更周密的考慮——參見條目44),但是內聯還會帶來其他的開銷,這就是我們即將討論的問題。

            inline是對編譯器的一次請求,但編譯器可能會忽略它。”在我們的討論開始之前,我們首先要弄清這一點。大多數編譯器如果認為當前的函數過于復雜(比如包含循環或遞歸的函數),或者這個函數是虛函數(即使是最平常的虛函數調用),就會拒絕將其內聯。后一個結論很好理解。因為virtual意味著“等到運行時再指出要調用哪個程序,”而inline意味著“在執行程序之前,使用要調用的函數來代替這次調用。”如果編譯器不知道要調用哪個函數,那么它們拒絕內聯函數體的做法就無可厚非了。

            綜上所述,我們得出下面的結論:一個給定的內聯函數是否真正的得到內聯,取決于你正在使用的構建環境——主要是編譯器。幸運的是,大多數編譯器擁有診斷機制,如果在內聯某個函數時失敗了,那么編譯器將會做出警告(參見條目53)。

            有些時候,即使編譯器認為某個函數非常適合內聯,可是還是會為它生成一個函數體。舉例說,如果你的程序要取得某個內聯函數的地址,那么一般情況下編譯器必須為其創建一個外聯的函數體。編譯器怎么能讓一個指針去指向一個不存在的函數呢?再加上“編譯器一般不會通過對函數指針的調用進行內聯”這一事實,更能肯定這一結論:對于一個內聯函數的調用是否應該得到內聯,取決于這一調用是如何進行的:

            inline void f() {...}              // 假設編譯器樂意于將f的調用進行內聯

            void (*pf)() = f;                  // pf指向f

            ...

            f();                               // 此調用將被內聯,因為這是一次“正常”的調用

             

            pf();                              // 此調用很可能不會被內聯,

                                               // 因為它是通過一個函數指針進行的

            即使你從未使用函數指針,未得到內聯處理的內聯函數依然會“陰魂不散”,這是因為調用函數指針的不僅僅是程序員。在對數組內的對象進行構造或析構的過程中,編譯器有時會生成構造函數和析構函數的不恰當的版本,從而使它們可以得到這些函數的指針以便使用。

            實際上,為構造函數和析構函數進行內聯通常不是一個最佳選擇。請看下面示例中Derived類的構造函數:

            class Base {

            public:

             ...

            private:

               std::string bm1, bm2;           // 基類成員12

            };

             

            class Derived: public Base {

            public:

              Derived() {}                     // 派生類的構造函數為空還有別的可能?

              ...

            private:

              std::string dm1, dm2, dm3;       // 派生類成員 1–3

            };

            乍看上去,將這個構造函數進行內聯再適合不過了,因為它不包含任何代碼。但是你的眼睛欺騙了你。

            C++對于在創建和銷毀對象的過程中發生的事件進行了多方面的保證。比如,當你使用new時,你動態創建的對象就會通過構造函數將其自動初始化;當你使用delete時,將調用相關的析構函數。當你創建一個對象時,該對象的所有基類和所有數據成員將自動得到構造,在銷毀這個對象時,相關的析構過程將會以反方向自動進行。如果在對象的構造過程中有異常拋出,那么對象中已經得到構造的部分將統統被自動銷毀。在所有這些場景中,C++告訴你什么一定會發生,但它沒有說明如何發生。這一點取決于編譯器的實現者,但是必須要清楚的一點是,這些事情并不是自發的。你必須要在程序中添加一些代碼來實現它們。這些代碼一定存在于某處,它們由編譯器代勞,在編譯過程中插入你的程序中。一些時候它們就存在于構造函數和析構函數中,所以,上文中Derived構造函數雖然看似是空的,但我們可以想象其與以下的具體實現中生成的代碼是等價的:

            Derived::Derived()                    // Derived“空”構造函數實現:概念版

            {

             Base::Base();                        // 初始化Base部分

             

             try { dm1.std::string::string(); }   // 嘗試構造dm1

             catch (...) {                        // 如果拋出異常,

               Base::~Base();                     // 銷毀基類部分,

               throw;                             // 并且傳播該異常

             }

             

             try { dm2.std::string::string(); }   // 嘗試構造dm2

             catch(...) {                         // 如果拋出異常,

               dm1.std::string::~string();        // 銷毀dm1,

               Base::~Base();                     // 銷毀基類部分,

               throw;                             // 并且傳播該異常

             }

             

             try { dm3.std::string::string(); }   // 嘗試構造dm3

             catch(...) {                         // 如果拋出異常,

               dm2.std::string::~string();        // 銷毀dm2,

               dm1.std::string::~string();        // 銷毀dm1,

               Base::~Base();                     // 銷毀基類部分,

               throw;                             // 并且傳播該異常

             }

            }

            這段代碼并不能完全反映出真實的編譯器所做的事情,因為真實的編譯器采用的做法更加精密。然而,上面的代碼可以精確地反映出Derived的“空”構造函數必須提供的內容。無論編譯器處理異常的實現方式多么精密,Derived的構造函數必須至少為其數據成員和基類調用構造函數,這些調用(可能就是內聯的)會使Derived顯得不那么適合進行內聯。

            這一推理過程對于Base的構造函數同樣適用,因此如果將Base內聯,所有添加進其中的代碼同樣也會添加進Derived的構造函數中(通過Derived構造函數調用Base構造函數的過程)。同時,如果string的構造函數恰巧是內聯的,那么Derived的構造函數將為其復制出五份副本,分別對應Derived對象中包含的五個字符串(兩個繼承而來,另外三個系對象本身包括)。至于為什么“Derived的構造函數是否該內聯須深思熟慮”,現在可能就很容易理解了。對于Derived的析構函數也一樣,無論如何,由Derived的構造函數所初始化的所有對象必須全部恰當的得到銷毀。

            庫設計者必須估算出將函數內聯所帶來的影響,因為你無法為庫中的客戶可見的內聯函數提供目標代碼級別的升級。換句話說,如果f是庫中的一個內聯函數,那么庫的客戶就會將f的函數體編譯進他們的程序中。如果一個庫實現者在后期修改了f的內容,那么所有曾經使用過f的客戶必須要重新編譯。這一點是我們所不希望看到的。另一個角度講,如果f不是內聯函數,那么修改f只需要客戶重新連接一下就可以了。這樣要比重新編譯減少很多繁雜的工作,并且,如果庫中需要使用的函數是動態鏈接的,那么它對于客戶就是完全透明的。

            從開發優質程序的角度講,這些重要問題應時刻牢記。但是以編寫代碼實際操作的角度來說,這一個事實將淹沒一切:大多數調試人員面對內聯函數時會遇到麻煩。這并不會令人意外,你如何為一個不存在的函數設定一個跟蹤點呢?盡管一些構建環境提供了內聯函數調試的支持,但是大多數環境都在調試過程中直接禁止內聯。

            對于“哪個函數應該聲明為inline而哪些不應該”這一問題,我們可以由上文中引出一個邏輯上的策略。起初,不要內聯任何內容,或者僅挑選出那些不得不內聯的函數(參見條目46)或者那些確實是很細小的程序(比如本節開篇處出現的Person::age)進行內聯。謹慎引入內聯,你就為調試工作提供了方便,但是你仍然要為內聯擺正位置:它屬于手工的優化操作。不要忘記80-20經驗決定主義原則:一個典型的程序將花去80%的時間僅僅運行20%的代碼。這是一個非常重要的原則,因為它提醒著我們軟件開發者的目標:找出你的代碼中20%的這部分進行優化,從而從整體上提高程序的性能。你可以花費漫長的時間進行內聯、修改函數等等,但如果你沒有鎖定正確的目標,那么你做再多的努力也是徒勞。

            時刻牢記

            僅僅對小型的、調用頻率高的程序進行內聯。這將簡化你的調試操作,提供更靈活的目標代碼可更新性,降低潛在的代碼膨脹發生的可能,并且可以讓程序獲得更高的速度。

            不要將模板聲明為inline的,因為它們一般在頭文件中出現。

            posted on 2007-11-18 23:27 ★ROY★ 閱讀(1588) 評論(1)  編輯 收藏 引用 所屬分類: Effective C++

            評論

            # re: 【讀書筆記】[Effective C++第3版][第30條] 深入探究內聯函數  回復  更多評論   

            我怎么看ACE的源碼里這么多宏,他還是高性能框架呢
            2007-11-19 21:22 | evoup
            久久亚洲国产成人影院网站| 2021国产精品午夜久久| 久久大香香蕉国产| 国产精品毛片久久久久久久| 久久久综合香蕉尹人综合网| 97久久国产露脸精品国产| 88久久精品无码一区二区毛片 | 久久久精品日本一区二区三区| 欧美日韩精品久久久久| 亚洲精品乱码久久久久久蜜桃不卡| 久久精品国产精品青草| 一本久久综合亚洲鲁鲁五月天亚洲欧美一区二区 | 日本一区精品久久久久影院| 要久久爱在线免费观看| 91久久精品国产成人久久| 亚洲级αV无码毛片久久精品| 久久精品国产精品亚洲下载| 少妇久久久久久被弄高潮| 久久久噜噜噜久久| 亚洲国产精品久久久久| 欧美一区二区三区久久综合| 四虎影视久久久免费观看| 国内精品欧美久久精品| 亚洲伊人久久大香线蕉苏妲己| 中文字幕无码精品亚洲资源网久久 | 国产精品青草久久久久福利99| 亚洲午夜无码久久久久| 亚洲精品国产自在久久| 久久中文字幕无码专区| 久久精品一区二区三区中文字幕| 久久精品国产亚洲欧美| 久久福利青草精品资源站免费| 人人狠狠综合久久88成人| 日韩精品久久无码中文字幕| 久久久一本精品99久久精品88| 亚洲色欲久久久久综合网| 无码国内精品久久人妻麻豆按摩| 精品久久久久中文字| 少妇久久久久久被弄到高潮| 色偷偷88欧美精品久久久| 亚洲欧美一区二区三区久久|