C++最佳實(shí)踐 | 6. 性能
本系列是開源書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_ptr和shared_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_ptr和make_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及以后版本,考慮在if和switch語句中初始化變量:
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::tuple、std::any和std::variant等對象。
微信公眾號:DeepNoMind
參考資料
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
