<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          C++最佳實(shí)踐 | 6. 性能

          共 8911字,需瀏覽 18分鐘

           ·

          2022-10-15 09:04

          本系列是開源書C++ Best Practises[1]的中文版,全書從工具、代碼風(fēng)格、安全性、可維護(hù)性、可移植性、多線程、性能、正確性等角度全面介紹了現(xiàn)代C++項(xiàng)目的最佳實(shí)踐。本文是該系列的第六篇。

          C++最佳實(shí)踐:

          1. 工具

          2. 代碼風(fēng)格

          3. 安全性

          4. 可維護(hù)性

          5. 可移植性及多線程

          6. 性能(本文)

          7. 正確性和腳本

          性能

          盡量使用前置聲明

          使用這種聲明方式:

          // some header file
          class MyClass;

          void doSomething(const MyClass &);

          而不是這樣:

          // some header file
          #include "MyClass.hpp"

          void doSomething(const MyClass &);

          同樣也使用于模板:

          template<typename T> class MyTemplatedType;

          這種方式可以主動(dòng)減少編譯時(shí)間并重新構(gòu)建依賴關(guān)系。

          注意: 前置聲明會(huì)阻礙內(nèi)聯(lián)和優(yōu)化,建議在發(fā)布版本中使用鏈接時(shí)優(yōu)化或鏈接時(shí)代碼生成。

          避免不必要的模板實(shí)例化

          模板不要隨便實(shí)例化,實(shí)例化過多模板,或者模板代碼多于必要的數(shù)量,會(huì)增加編譯代碼的大小和構(gòu)建時(shí)間。

          更多示例請參考: Template Code Bloat Revisited: A Smaller make_shared[2]

          避免遞歸模板實(shí)例化

          遞歸模板實(shí)例化可能會(huì)給編譯器帶來很大的負(fù)擔(dān),并且代碼更加難以理解。

          如果可能的話,考慮使用可變參數(shù)展開和折疊[3]。

          分析構(gòu)建

          可以使用Templight[4]工具分析項(xiàng)目的構(gòu)建時(shí)間,它需要花一些時(shí)間來構(gòu)建,但一旦這樣做了,可以用來替換clang++。

          使用Templight進(jìn)行構(gòu)建之后,需要對結(jié)果進(jìn)行分析,templight-tools[5]項(xiàng)目提供了各種方法(建議使用callgrind轉(zhuǎn)換并使用kcachegrind對結(jié)果進(jìn)行可視化)。

          隔離頻繁更改的頭文件

          不要包含不需要的頭文件

          編譯器必須處理看到的每個(gè)include指令,即使只是在看到#ifndefinclude保護(hù)符后立即停止,仍然必須打開文件并進(jìn)行處理。

          include-what-you-use[6]是一個(gè)可以幫我們確定需要哪些頭文件的工具。

          減少預(yù)處理器的工作

          這是“隔離頻繁更改的頭文件”和“不要包含不需要的頭文件”的一般形式。類似BOOST_PP這樣的工具可能非常有用,但也給預(yù)處理器帶來了巨大的負(fù)擔(dān)。

          考慮使用預(yù)編譯頭文件

          使用預(yù)編譯頭文件可以大大減少大型項(xiàng)目的編譯時(shí)間,選定的頭文件被編譯成中間形式(PCH文件),編譯器可以更快處理。建議只將經(jīng)常使用但很少更改的頭文件定義為預(yù)編譯頭文件(例如系統(tǒng)頭文件和庫頭文件),以減少編譯時(shí)間。但必須記住,使用預(yù)編譯頭文件有幾個(gè)缺點(diǎn):

          • 預(yù)編譯頭文件不可移植。
          • 生成的PCH文件依賴于機(jī)器。
          • 生成的PCH文件可能相當(dāng)大。
          • 它會(huì)破壞頭文件依賴關(guān)系。由于有預(yù)編譯頭文件,每個(gè)文件都有可能包含標(biāo)記為預(yù)編譯頭文件的每個(gè)頭文件。因此,如果禁用預(yù)編譯頭文件,可能會(huì)導(dǎo)致構(gòu)建失敗。如果需要發(fā)布庫之類的項(xiàng)目,這可能是個(gè)問題。正因?yàn)槿绱?,?qiáng)烈建議在第一次構(gòu)建時(shí)啟用預(yù)編譯頭,而在后續(xù)構(gòu)建時(shí)將其關(guān)閉。

          大多數(shù)常見的編譯器都支持預(yù)編譯頭文件,比如GCC[7]、Clang[8]Visual Studio[9]。像cotire[10](cmake的插件)這樣的工具可以幫助我們在構(gòu)建系統(tǒng)中添加預(yù)編譯的頭文件。

          考慮使用工具

          工具并不意味著可以取代好的設(shè)計(jì)。

          • ccache[11],用于類unix操作系統(tǒng)的編譯結(jié)果緩存
          • clcache[12],cl.exe的編譯結(jié)果緩存(MSVC)
          • warp[13],F(xiàn)acebook的預(yù)處理器

          將tmp放在Ramdisk上

          詳見YouTube視頻: https://www.youtube.com/watch?v=t4M3yG1dWho

          使用gold鏈接器

          如果是在Linux上,考慮使用GCC的gold鏈接器(ld.gold)。

          參考: gold: Google Releases New and Improved GCC Linker[14]

          運(yùn)行時(shí)

          分析代碼

          在不分析代碼的情況下,無法真正找到瓶頸在哪里。

          • http://developer.amd.com/tools-and-sdks/opencl-zone/codexl/
          • http://www.codersnotes.com/sleepy

          簡化代碼

          代碼越清晰、越簡單、越容易閱讀,編譯器就越有可能更好的將其實(shí)現(xiàn)。

          使用初始化列表

          // This
          std::vector<ModelObject> mos{mo1, mo2};

          // -or-
          auto mos = std::vector<ModelObject>{mo1, mo2};
          // Don't do this
          std::vector<ModelObject> mos;
          mos.push_back(mo1);
          mos.push_back(mo2);

          通過減少對象復(fù)制并調(diào)整容器大小,初始化列表能顯著提升性能。

          減少臨時(shí)對象

          // Instead of
          auto mo1 = getSomeModelObject();
          auto mo2 = getAnotherModelObject();

          doSomething(mo1, mo2);
          // consider:
          doSomething(getSomeModelObject(), getAnotherModelObject());

          這類代碼將阻礙編譯器執(zhí)行move操作……

          啟用移動(dòng)(move)操作

          move操作是C++11中最受歡迎的特性之一,該操作允許編譯器通過移動(dòng)臨時(shí)對象從而避免額外的拷貝。

          某些代碼(例如聲明自己的析構(gòu)函數(shù)或賦值操作符或拷貝構(gòu)造函數(shù))會(huì)阻止編譯器生成移動(dòng)構(gòu)造函數(shù)。

          對于大多數(shù)代碼,下面這么一個(gè)簡單的定義:

          ModelObject(ModelObject &&) = default;

          ...就足夠了,不過MSVC2013似乎不支持這段代碼。

          避免shared_ptr拷貝

          shared_ptr對象的拷貝成本比想象的要高得多,因?yàn)橐糜?jì)數(shù)必須是原子的和線程安全的。這條規(guī)則只是再次強(qiáng)調(diào)了上面的注意事項(xiàng): 避免臨時(shí)對象和過多的對象副本。僅僅因?yàn)槲覀兪褂昧藀Impl,并不意味著副本沒有代價(jià)。

          盡可能減少拷貝和重分配

          對于更簡單的情況,可以使用三元操作符:

          // Bad Idea
          std::string somevalue;

          if (caseA) {
            somevalue = "Value A";
          else {
            somevalue = "Value B";
          }
          // Better Idea
          const std::string somevalue = caseA ? "Value A" : "Value B";

          使用立即調(diào)用的lambda[15]可以簡化更復(fù)雜的情況。

          // Bad Idea
          std::string somevalue;

          if (caseA) {
            somevalue = "Value A";
          else if(caseB) {
            somevalue = "Value B";
          else {
            somevalue = "Value C";
          }
          // Better Idea
          const std::string somevalue = [&]( "&"){
              if (caseA) {
                return "Value A";
              } else if (caseB) {
                return "Value B";
              } else {
                return "Value C";
              }
            }();

          避免多余的異常

          在正常處理期間,內(nèi)部拋出和捕獲的異常會(huì)降低應(yīng)用程序的執(zhí)行速度。由于調(diào)試器會(huì)監(jiān)視和報(bào)告每個(gè)異常事件,因此還會(huì)破壞調(diào)試器的用戶體驗(yàn)。最好盡可能避免內(nèi)部異常處理。

          拋棄new

          我們已經(jīng)知道不該使用裸內(nèi)存訪問,因此改用unique_ptrshared_ptr,對吧?堆分配比棧分配昂貴得多,但有時(shí)不得不用。更糟的是,創(chuàng)建shared_ptr實(shí)際上需要在堆上分配2次。

          然而,make_shared函數(shù)可以將其減少為一次。

          std::shared_ptr<ModelObject_Impl>(new ModelObject_Impl());

          // should become
          std::make_shared<ModelObject_Impl>(); // (it's also more readable and concise)

          優(yōu)先選擇unique_ptr而不是shared_ptr

          可能的話,使用unique_ptr而不是shared_ptr。unique_ptr是不可復(fù)制的,因此不需要跟蹤副本,比shared_ptr性能更好。另外,類似于shared_ptrmake_shared的關(guān)系,應(yīng)該使用make_unique(C++14或更高版本)來創(chuàng)建unique_ptr:

          std::make_unique<ModelObject_Impl>();

          目前的最佳實(shí)踐也建議從工廠函數(shù)返回unique_ptr,然后在必要時(shí)將unique_ptr轉(zhuǎn)換為shared_ptr。

          std::unique_ptr<ModelObject_Impl> factory();

          auto shared = std::shared_ptr<ModelObject_Impl>(factory());

          拋棄std::endl

          std::endl表示刷新操作,等價(jià)于"\n" << std::flush。

          限制變量作用域

          變量應(yīng)該盡可能晚聲明,最好只在可以初始化對象時(shí)聲明。減小變量作用域可以減少內(nèi)存的使用,提高代碼效率,并幫助編譯器進(jìn)一步優(yōu)化代碼。

          // Good Idea
          for (int i = 0; i < 15; ++i)
          {
            MyObject obj(i);
            // do something with obj
          }

          // Bad Idea
          MyObject obj; // meaningless object initialization
          for (int i = 0; i < 15; ++i)
          {
            obj = MyObject(i); // unnecessary assignment operation
            // do something with obj
          }
          // obj is still taking up memory for no reason

          對于C++17及以后版本,考慮在ifswitch語句中初始化變量:

          if (MyObject obj(index); obj.good()) {
              // do something if obj is good
          else {
              // do something if obj is not good
          }

          Github上對此有專門的討論: https://github.com/lefticus/cppbestpractices/issues/52

          優(yōu)先選擇double類型而不是float類型,但需要先測試

          根據(jù)情況和編譯器的優(yōu)化能力,一種可能比另一種更快。選擇float意味著精度較低,并可能由于類型轉(zhuǎn)換而影響性能。在可向量化操作中,如果能夠犧牲精度,float可能更快。

          double是C++中浮點(diǎn)值的默認(rèn)類型,因此推薦作為默認(rèn)選項(xiàng)。

          參考下面的文章獲取更多信息: double or float, which is faster?[16]

          優(yōu)先選擇++i而不是i++

          ...當(dāng)語義正確時(shí),前置自增比后置自增更快[17],因?yàn)榍爸米栽霾恍枰獎(jiǎng)?chuàng)建對象副本。

          // Bad Idea
          for (int i = 0; i < 15; i++)
          {
            std::cout << i << '\n';
          }

          // Good Idea
          for (int i = 0; i < 15; ++i)
          {
            std::cout << i << '\n';
          }

          即使許多現(xiàn)代編譯器將這兩個(gè)循環(huán)優(yōu)化為相同的匯編代碼,選擇++i仍然是一種良好的實(shí)踐。你永遠(yuǎn)無法確定代碼會(huì)不會(huì)使用不帶優(yōu)化的編譯器,因此沒有任何理由不這樣做。此外,編譯器有可能只對整數(shù)類型進(jìn)行優(yōu)化,而不一定對所有迭代器或其他用戶自定義類型進(jìn)行優(yōu)化。

          總而言之,如果前置自增操作符與后置自增操作符在語義上相同,那么使用前置自增操作符總是更好。

          char是char, string是string

          // Bad Idea
          std::cout << someThing() << "\n";

          // Good Idea
          std::cout << someThing() << '\n';

          看上去區(qū)別不大,但是"\n"必須被編譯器解析為const char *,必須在寫入流(或附加到字符串)時(shí)對\0進(jìn)行范圍檢查,而'\n'是已知的單個(gè)字符,可以節(jié)約許多CPU指令。

          如果多次調(diào)用效率低下的代碼,可能會(huì)對性能產(chǎn)生影響,更重要的是,考慮這兩種使用情況會(huì)讓我們更多的考慮編譯器和運(yùn)行時(shí)在執(zhí)行代碼時(shí)必須做什么。

          永遠(yuǎn)不要用std::bind

          std::bind的開銷(包括編譯時(shí)和運(yùn)行時(shí))幾乎總是比需要的更多,相反,我們只需使用lambda。

          // Bad Idea
          auto f = std::bind(&my_function, "hello"std::placeholders::_1);
          f("world");

          // Good Idea
          auto f = [](const std::string &s) { return my_function("hello", s); };
          f("world");

          了解標(biāo)準(zhǔn)庫

          正確使用供應(yīng)商提供的標(biāo)準(zhǔn)庫中已經(jīng)高度優(yōu)化的組件。

          in_place_t及相關(guān)內(nèi)容

          知道如何使用in_place_t和相關(guān)標(biāo)簽高效創(chuàng)建諸如std::tuplestd::anystd::variant等對象。

          微信公眾號:DeepNoMind

          參考資料

          [1]

          C++ Best Practises: https://lefticus.gitbooks.io/cpp-best-practices/content/

          [2]

          Template Code Bloat Revisited: A Smaller make_shared: https://articles.emptycrate.com/2015/04/27/template_code_bloat_revisited_a_smaller_makeshared.html

          [3]

          Folds (ish) In C++11: http://articles.emptycrate.com/2016/05/14/folds_in_cpp11_ish.html

          [4]

          Templight: https://github.com/mikael-s-persson/templight

          [5]

          templight-tools: https://github.com/mikael-s-persson/templight-tools

          [6]

          include-what-you-use: https://github.com/include-what-you-use/include-what-you-use

          [7]

          GCC: https://gcc.gnu.org/onlinedocs/gcc/Precompiled-Headers.html

          [8]

          Clang: http://clang.llvm.org/docs/PCHInternals.html

          [9]

          Visual Studio: https://msdn.microsoft.com/en-us/library/szfdksca.aspx

          [10]

          cotire: https://github.com/sakra/cotire/

          [11]

          ccache: https://ccache.samba.org/

          [12]

          clcache: https://github.com/frerich/clcache

          [13]

          warp: https://github.com/facebook/warp

          [14]

          gold: Google Releases New and Improved GCC Linker: https://opensource.googleblog.com/2008/04/gold-google-releases-new-and-improved.html

          [15]

          Complex Object Initialization Optimization with IIFE in C++11: http://blog2.emptycrate.com/content/complex-object-initialization-optimization-iife-c11

          [16]

          double or float, which is faster?: https://stackoverflow.com/questions/4584637/double-or-float-which-is-faster

          [17]

          Why is ++i faster than i++ in C++?: http://blog2.emptycrate.com/content/why-i-faster-i-c

          - END -
          打擾了,最近搞了編程學(xué)習(xí)的圈子。邀請了幾...
          點(diǎn)擊閱讀原文加入知識星球

          瀏覽 51
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  就爱干一区二区三区 | 成人电影一区 | 婷婷激情五月综合 | 亚洲高清视频不卡无码视频免费在线观看 | 91国产大片|