本系列是開源書C++ Best Practises[1]的中文版,全書從工具、代碼風格、安全性、可維護性、可移植性、多線程、性能、正確性等角度全面介紹了現代C++項目的最佳實踐。本文是該系列的第六篇。
C++最佳實踐:
1. 工具
2. 代碼風格
3.安全性
4.可維護性
5.可移植性及多線程
6.性能(本文)
7.正確性和腳本
性能
盡量使用前置聲明
使用這種聲明方式:
//someheaderfile classMyClass; voiddoSomething(constMyClass&);
而不是這樣:
//someheaderfile #include"MyClass.hpp" voiddoSomething(constMyClass&);
同樣也使用于模板:
templateclassMyTemplatedType;
這種方式可以主動減少編譯時間并重新構建依賴關系。
注意: 前置聲明會阻礙內聯和優化,建議在發布版本中使用鏈接時優化或鏈接時代碼生成。
避免不必要的模板實例化
模板不要隨便實例化,實例化過多模板,或者模板代碼多于必要的數量,會增加編譯代碼的大小和構建時間。
更多示例請參考: Template Code Bloat Revisited: A Smaller make_shared[2]
避免遞歸模板實例化
遞歸模板實例化可能會給編譯器帶來很大的負擔,并且代碼更加難以理解。
如果可能的話,考慮使用可變參數展開和折疊[3]。
分析構建
可以使用Templight[4]工具分析項目的構建時間,它需要花一些時間來構建,但一旦這樣做了,可以用來替換clang++。
使用Templight進行構建之后,需要對結果進行分析,templight-tools[5]項目提供了各種方法(建議使用callgrind轉換并使用kcachegrind對結果進行可視化)。
隔離頻繁更改的頭文件
不要包含不需要的頭文件
編譯器必須處理看到的每個include指令,即使只是在看到#ifndefinclude保護符后立即停止,仍然必須打開文件并進行處理。
include-what-you-use[6]是一個可以幫我們確定需要哪些頭文件的工具。
減少預處理器的工作
這是“隔離頻繁更改的頭文件”和“不要包含不需要的頭文件”的一般形式。類似BOOST_PP這樣的工具可能非常有用,但也給預處理器帶來了巨大的負擔。
考慮使用預編譯頭文件
使用預編譯頭文件可以大大減少大型項目的編譯時間,選定的頭文件被編譯成中間形式(PCH文件),編譯器可以更快處理。建議只將經常使用但很少更改的頭文件定義為預編譯頭文件(例如系統頭文件和庫頭文件),以減少編譯時間。但必須記住,使用預編譯頭文件有幾個缺點:
預編譯頭文件不可移植。
生成的PCH文件依賴于機器。
生成的PCH文件可能相當大。
它會破壞頭文件依賴關系。由于有預編譯頭文件,每個文件都有可能包含標記為預編譯頭文件的每個頭文件。因此,如果禁用預編譯頭文件,可能會導致構建失敗。如果需要發布庫之類的項目,這可能是個問題。正因為如此,強烈建議在第一次構建時啟用預編譯頭,而在后續構建時將其關閉。
大多數常見的編譯器都支持預編譯頭文件,比如GCC[7]、Clang[8]和Visual Studio[9]。像cotire[10](cmake的插件)這樣的工具可以幫助我們在構建系統中添加預編譯的頭文件。
考慮使用工具
工具并不意味著可以取代好的設計。
ccache[11],用于類unix操作系統的編譯結果緩存
clcache[12],cl.exe的編譯結果緩存(MSVC)
warp[13],Facebook的預處理器
將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]
運行時
分析代碼
在不分析代碼的情況下,無法真正找到瓶頸在哪里。
http://developer.amd.com/tools-and-sdks/opencl-zone/codexl/
http://www.codersnotes.com/sleepy
簡化代碼
代碼越清晰、越簡單、越容易閱讀,編譯器就越有可能更好的將其實現。
使用初始化列表
//This std::vectormos{mo1,mo2}; //-or- automos=std::vector {mo1,mo2};
//Don'tdothis std::vectormos; mos.push_back(mo1); mos.push_back(mo2);
通過減少對象復制并調整容器大小,初始化列表能顯著提升性能。
減少臨時對象
//Insteadof automo1=getSomeModelObject(); automo2=getAnotherModelObject(); doSomething(mo1,mo2);
//consider: doSomething(getSomeModelObject(),getAnotherModelObject());
這類代碼將阻礙編譯器執行move操作……
啟用移動(move)操作
move操作是C++11中最受歡迎的特性之一,該操作允許編譯器通過移動臨時對象從而避免額外的拷貝。
某些代碼(例如聲明自己的析構函數或賦值操作符或拷貝構造函數)會阻止編譯器生成移動構造函數。
對于大多數代碼,下面這么一個簡單的定義:
ModelObject(ModelObject&&)=default;
...就足夠了,不過MSVC2013似乎不支持這段代碼。
避免shared_ptr拷貝
shared_ptr對象的拷貝成本比想象的要高得多,因為引用計數必須是原子的和線程安全的。這條規則只是再次強調了上面的注意事項: 避免臨時對象和過多的對象副本。僅僅因為我們使用了pImpl,并不意味著副本沒有代價。
盡可能減少拷貝和重分配
對于更簡單的情況,可以使用三元操作符:
//BadIdea std::stringsomevalue; if(caseA){ somevalue="ValueA"; }else{ somevalue="ValueB"; }
//BetterIdea conststd::stringsomevalue=caseA?"ValueA":"ValueB";
使用立即調用的lambda[15]可以簡化更復雜的情況。
//BadIdea std::stringsomevalue; if(caseA){ somevalue="ValueA"; }elseif(caseB){ somevalue="ValueB"; }else{ somevalue="ValueC"; }
//BetterIdea conststd::stringsomevalue=[&]("&"){ if(caseA){ return"ValueA"; }elseif(caseB){ return"ValueB"; }else{ return"ValueC"; } }();
避免多余的異常
在正常處理期間,內部拋出和捕獲的異常會降低應用程序的執行速度。由于調試器會監視和報告每個異常事件,因此還會破壞調試器的用戶體驗。最好盡可能避免內部異常處理。
拋棄new
我們已經知道不該使用裸內存訪問,因此改用unique_ptr和shared_ptr,對吧?堆分配比棧分配昂貴得多,但有時不得不用。更糟的是,創建shared_ptr實際上需要在堆上分配2次。
然而,make_shared函數可以將其減少為一次。
std::shared_ptr(newModelObject_Impl()); //shouldbecome std::make_shared ();//(it'salsomorereadableandconcise)
優先選擇unique_ptr而不是shared_ptr
可能的話,使用unique_ptr而不是shared_ptr。unique_ptr是不可復制的,因此不需要跟蹤副本,比shared_ptr性能更好。另外,類似于shared_ptr和make_shared的關系,應該使用make_unique(C++14或更高版本)來創建unique_ptr:
std::make_unique();
目前的最佳實踐也建議從工廠函數返回unique_ptr,然后在必要時將unique_ptr轉換為shared_ptr。
std::unique_ptrfactory(); autoshared=std::shared_ptr (factory());
拋棄std::endl
std::endl表示刷新操作,等價于" " << std::flush。
限制變量作用域
變量應該盡可能晚聲明,最好只在可以初始化對象時聲明。減小變量作用域可以減少內存的使用,提高代碼效率,并幫助編譯器進一步優化代碼。
//GoodIdea for(inti=0;i15;?++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及以后版本,考慮在if和switch語句中初始化變量:
if(MyObjectobj(index);obj.good()){ //dosomethingifobjisgood }else{ //dosomethingifobjisnotgood }
Github上對此有專門的討論: https://github.com/lefticus/cppbestpractices/issues/52
優先選擇double類型而不是float類型,但需要先測試
根據情況和編譯器的優化能力,一種可能比另一種更快。選擇float意味著精度較低,并可能由于類型轉換而影響性能。在可向量化操作中,如果能夠犧牲精度,float可能更快。
double是C++中浮點值的默認類型,因此推薦作為默認選項。
參考下面的文章獲取更多信息: double or float, which is faster?[16]
優先選擇++i而不是i++
...當語義正確時,前置自增比后置自增更快[17],因為前置自增不需要創建對象副本。
//BadIdea for(inti=0;i15;?i++) { ??std::cout?<
即使許多現代編譯器將這兩個循環優化為相同的匯編代碼,選擇++i仍然是一種良好的實踐。你永遠無法確定代碼會不會使用不帶優化的編譯器,因此沒有任何理由不這樣做。此外,編譯器有可能只對整數類型進行優化,而不一定對所有迭代器或其他用戶自定義類型進行優化。
總而言之,如果前置自增操作符與后置自增操作符在語義上相同,那么使用前置自增操作符總是更好。
char是char, string是string
//BadIdea std::cout<
看上去區別不大,但是" "必須被編譯器解析為const char *,必須在寫入流(或附加到字符串)時對?進行范圍檢查,而' '是已知的單個字符,可以節約許多CPU指令。
如果多次調用效率低下的代碼,可能會對性能產生影響,更重要的是,考慮這兩種使用情況會讓我們更多的考慮編譯器和運行時在執行代碼時必須做什么。
永遠不要用std::bind
std::bind的開銷(包括編譯時和運行時)幾乎總是比需要的更多,相反,我們只需使用lambda。
//BadIdea autof=std::bind(&my_function,"hello",std::_1); f("world"); //GoodIdea autof=[](conststd::string&s){returnmy_function("hello",s);}; f("world");
了解標準庫
正確使用供應商提供的標準庫中已經高度優化的組件。
in_place_t及相關內容
知道如何使用in_place_t和相關標簽高效創建諸如std::tuple、std::any和std::variant等對象。
審核編輯:彭靜
-
C++
+關注
關注
22文章
2108瀏覽量
73623 -
代碼
+關注
關注
30文章
4780瀏覽量
68529 -
編譯器
+關注
關注
1文章
1624瀏覽量
49108
原文標題:C++最佳實踐 | 6. 性能
文章出處:【微信號:C語言與CPP編程,微信公眾號:C語言與CPP編程】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論