本系列是開源書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.正確性和腳本
性能
盡量使用前置聲明
使用這種聲明方式:
//someheaderfile classMyClass; voiddoSomething(constMyClass&);
而不是這樣:
//someheaderfile #include"MyClass.hpp" voiddoSomething(constMyClass&);
同樣也使用于模板:
templateclassMyTemplatedType;
這種方式可以主動(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í)間。
更多示例請(qǐng)參考: 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)建之后,需要對(duì)結(jié)果進(jìn)行分析,templight-tools[5]項(xiàng)目提供了各種方法(建議使用callgrind轉(zhuǎn)換并使用kcachegrind對(duì)結(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的插件)這樣的工具可以幫助我們?cè)跇?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
簡(jiǎn)化代碼
代碼越清晰、越簡(jiǎn)單、越容易閱讀,編譯器就越有可能更好的將其實(shí)現(xiàn)。
使用初始化列表
//This std::vectormos{mo1,mo2}; //-or- automos=std::vector {mo1,mo2};
//Don'tdothis std::vectormos; mos.push_back(mo1); mos.push_back(mo2);
通過減少對(duì)象復(fù)制并調(diào)整容器大小,初始化列表能顯著提升性能。
減少臨時(shí)對(duì)象
//Insteadof automo1=getSomeModelObject(); automo2=getAnotherModelObject(); doSomething(mo1,mo2);
//consider: doSomething(getSomeModelObject(),getAnotherModelObject());
這類代碼將阻礙編譯器執(zhí)行move操作……
啟用移動(dòng)(move)操作
move操作是C++11中最受歡迎的特性之一,該操作允許編譯器通過移動(dòng)臨時(shí)對(duì)象從而避免額外的拷貝。
某些代碼(例如聲明自己的析構(gòu)函數(shù)或賦值操作符或拷貝構(gòu)造函數(shù))會(huì)阻止編譯器生成移動(dòng)構(gòu)造函數(shù)。
對(duì)于大多數(shù)代碼,下面這么一個(gè)簡(jiǎn)單的定義:
ModelObject(ModelObject&&)=default;
...就足夠了,不過MSVC2013似乎不支持這段代碼。
避免shared_ptr拷貝
shared_ptr對(duì)象的拷貝成本比想象的要高得多,因?yàn)橐糜?jì)數(shù)必須是原子的和線程安全的。這條規(guī)則只是再次強(qiáng)調(diào)了上面的注意事項(xiàng): 避免臨時(shí)對(duì)象和過多的對(duì)象副本。僅僅因?yàn)槲覀兪褂昧?a href="http://www.nxhydt.com/tags/pi/" target="_blank">pImpl,并不意味著副本沒有代價(jià)。
盡可能減少拷貝和重分配
對(duì)于更簡(jiǎn)單的情況,可以使用三元操作符:
//BadIdea std::stringsomevalue; if(caseA){ somevalue="ValueA"; }else{ somevalue="ValueB"; }
//BetterIdea conststd::stringsomevalue=caseA?"ValueA":"ValueB";
使用立即調(diào)用的lambda[15]可以簡(jiǎn)化更復(fù)雜的情況。
//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"; } }();
避免多余的異常
在正常處理期間,內(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_ptr和shared_ptr,對(duì)吧?堆分配比棧分配昂貴得多,但有時(shí)不得不用。更糟的是,創(chuàng)建shared_ptr實(shí)際上需要在堆上分配2次。
然而,make_shared函數(shù)可以將其減少為一次。
std::shared_ptr(newModelObject_Impl()); //shouldbecome std::make_shared ();//(it'salsomorereadableandconcise)
優(yōu)先選擇unique_ptr而不是shared_ptr
可能的話,使用unique_ptr而不是shared_ptr。unique_ptr是不可復(fù)制的,因此不需要跟蹤副本,比shared_ptr性能更好。另外,類似于shared_ptr和make_shared的關(guān)系,應(yīng)該使用make_unique(C++14或更高版本)來創(chuàng)建unique_ptr:
std::make_unique();
目前的最佳實(shí)踐也建議從工廠函數(shù)返回unique_ptr,然后在必要時(shí)將unique_ptr轉(zhuǎn)換為shared_ptr。
std::unique_ptrfactory(); autoshared=std::shared_ptr (factory());
拋棄std::endl
std::endl表示刷新操作,等價(jià)于" " << std::flush。
限制變量作用域
變量應(yīng)該盡可能晚聲明,最好只在可以初始化對(duì)象時(shí)聲明。減小變量作用域可以減少內(nèi)存的使用,提高代碼效率,并幫助編譯器進(jìn)一步優(yōu)化代碼。
//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
對(duì)于C++17及以后版本,考慮在if和switch語句中初始化變量:
if(MyObjectobj(index);obj.good()){ //dosomethingifobjisgood }else{ //dosomethingifobjisnotgood }
Github上對(duì)此有專門的討論: https://github.com/lefticus/cppbestpractices/issues/52
優(yōu)先選擇double類型而不是float類型,但需要先測(cè)試
根據(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)建對(duì)象副本。
//BadIdea for(inti=0;i15;?i++) { ??std::cout?<
即使許多現(xiàn)代編譯器將這兩個(gè)循環(huán)優(yōu)化為相同的匯編代碼,選擇++i仍然是一種良好的實(shí)踐。你永遠(yuǎn)無法確定代碼會(huì)不會(huì)使用不帶優(yōu)化的編譯器,因此沒有任何理由不這樣做。此外,編譯器有可能只對(duì)整數(shù)類型進(jìn)行優(yōu)化,而不一定對(duì)所有迭代器或其他用戶自定義類型進(jìn)行優(yōu)化。
總而言之,如果前置自增操作符與后置自增操作符在語義上相同,那么使用前置自增操作符總是更好。
char是char, string是string
//BadIdea std::cout<
看上去區(qū)別不大,但是" "必須被編譯器解析為const char *,必須在寫入流(或附加到字符串)時(shí)對(duì)?進(jìn)行范圍檢查,而' '是已知的單個(gè)字符,可以節(jié)約許多CPU指令。
如果多次調(diào)用效率低下的代碼,可能會(huì)對(duì)性能產(chǎn)生影響,更重要的是,考慮這兩種使用情況會(huì)讓我們更多的考慮編譯器和運(yùn)行時(shí)在執(zhí)行代碼時(shí)必須做什么。
永遠(yuǎn)不要用std::bind
std::bind的開銷(包括編譯時(shí)和運(yùn)行時(shí))幾乎總是比需要的更多,相反,我們只需使用lambda。
//BadIdea autof=std::bind(&my_function,"hello",std::_1); f("world"); //GoodIdea autof=[](conststd::string&s){returnmy_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::tuple、std::any和std::variant等對(duì)象。
審核編輯:彭靜
-
C++
+關(guān)注
關(guān)注
22文章
2104瀏覽量
73489 -
代碼
+關(guān)注
關(guān)注
30文章
4747瀏覽量
68348 -
編譯器
+關(guān)注
關(guān)注
1文章
1618瀏覽量
49048
原文標(biāo)題:C++最佳實(shí)踐 | 6. 性能
文章出處:【微信號(hào):C語言與CPP編程,微信公眾號(hào):C語言與CPP編程】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論