C++ 核心指南 —— 性能
C++ 核心指南 —— 性能
閱讀建議:先閱讀 《性能優(yōu)化的一般策略及方法》
截至目前,C++ Core Guidelines 中關(guān)于性能優(yōu)化的建議共有 18 條,而其中很大一部分是告誡你,不要輕易優(yōu)化!
非必要,不優(yōu)化
- Per.1: 不要無故優(yōu)化
- Per.2: 不要過早優(yōu)化
- Per.3: 只優(yōu)化少數(shù)關(guān)鍵代碼
前三條可以總結(jié)為:非必要,不優(yōu)化。所謂的“優(yōu)化”,是指犧牲可讀性、可維護性,以換取性能提升(否則應(yīng)該作為編程的標準實踐)。優(yōu)化可能引入新的 bug,增加維護成本。軟件工程師應(yīng)把重心放在編寫簡潔、易于理解和維護的代碼,而不是把性能作為首要目標。
先測量,再優(yōu)化
如果性能非常重要,應(yīng)該通過精確地測量,找到程序的 hot spots,再有針對性地優(yōu)化。
Per.4: 不要假設(shè)復(fù)雜的代碼比簡單的代碼快
- 多線程未必比單線程快:考慮到線程間同步的開銷、上下文切換開銷,多線程未必比單線程快
- 利用一系列復(fù)雜的優(yōu)化技巧編寫的復(fù)雜代碼未必比直接編寫的簡單代碼快,如
// 好:簡單直接
vector<uint8_t> v(100000);
for (auto& c : v)
c = ~c;
// 不好:復(fù)雜的優(yōu)化技巧,本意想更快,但往往更慢!
vector<uint8_t> v(100000);
for (size_t i = 0; i < v.size(); i += sizeof(uint64_t)) {
uint64_t& quad_word = *reinterpret_cast<uint64_t*>(&v[i]);
quad_word = ~quad_word;
}
Per.5: 不要假設(shè)低級語言比高級語言快
不要低估編譯器的優(yōu)化能力,很多時候編譯器產(chǎn)生的代碼要比手動編寫低級語言更高效!
Per.6: 沒有測量就不要對性能妄下斷言
- 性能優(yōu)化很多時候是反直覺的,針對某些條件下的性能優(yōu)化技巧在另一個環(huán)境下可能會劣化性能,因此必須要測量才知道某個改動到底會“優(yōu)化”還是“劣化”性能
- 小于 4% 的代碼能占用 50% 的程序執(zhí)行時間。只有測量才知道時間花在哪里,才能有針對性地優(yōu)化
以上 6 條建議在 《性能優(yōu)化的一般策略及方法》 中有更詳細的描述。
具體優(yōu)化建議
Per.7 設(shè)計應(yīng)當允許優(yōu)化
總是需要優(yōu)化最初的設(shè)計,如果設(shè)計之初完全忽視了將來優(yōu)化的可能性,會導(dǎo)致很難修改。
過早優(yōu)化是萬惡之源,但這并不是輕視性能的借口。一些經(jīng)過實踐檢驗的最佳實踐可以幫助我們寫出高效、可維護、可優(yōu)化的代碼:
- 信息傳遞:接口設(shè)計要干凈,但還要攜帶足夠的信息,以便后續(xù)改進實現(xiàn)。
- 緊湊的數(shù)據(jù)結(jié)構(gòu):默認情況下,使用緊湊的數(shù)據(jù)結(jié)構(gòu),如
std::vector,如果你認為需要一個鏈表,嘗試設(shè)計接口,使用戶看不到這個結(jié)構(gòu)(參考標準庫算法的接口設(shè)計)。 - 函數(shù)參數(shù)的傳遞和返回:區(qū)分可變和不可變數(shù)據(jù)。不要把 資源管理 的任務(wù)強加給用戶。不要把假想的 indirection 強加給用戶。使用常規(guī)的方式傳遞信息,非常規(guī)或為特定實現(xiàn)“優(yōu)化”過的數(shù)據(jù)傳遞方式可能會導(dǎo)致后續(xù)難以修改實現(xiàn)。
- 抽象:不要過度泛化。試圖滿足每種可能的使用情況(包括誤用),把每個設(shè)計決策推遲(編譯或運行時 indirection)會導(dǎo)致復(fù)雜、臃腫、難以理解。不要基于對未來需求的猜測來進行泛化,從具體示例中進行泛化。泛化時保持性能,理想狀態(tài)是零開銷泛化。
- 庫:選擇具有良好接口設(shè)計的庫。如果沒有現(xiàn)成的,自己寫一個,模仿具有良好接口風格的庫(可以從標準庫找靈感)。
- 隔離:把你的代碼和舊的、亂的代碼隔離開。可以按照自己的風格,設(shè)計一個接口風格良好的 wrapper,把那些不得不用的舊的、亂的代碼封裝起來,不要污染到我們自己的代碼。
"indirection"(間接)通常指的是通過引入額外的層級或中介來訪問數(shù)據(jù)或功能。在 C++ 中,這可能涉及使用指針、引用或其他間接方式來訪問變量、對象或函數(shù)。
注
- 設(shè)計接口時,不要只考慮第一版的用例和實現(xiàn)。初版實現(xiàn)之后,必須 review,因為一旦部署之后,彌補錯誤將很困難。
- 低級語言并不總是高效。高級語言的代碼不一定慢。
- 任何操作都有開銷,不用過分擔心開銷(現(xiàn)代計算機都足夠的快),但是需要大致了解各種操作的開銷。例如:內(nèi)存訪問、函數(shù)調(diào)用、字符串比較、系統(tǒng)調(diào)用、磁盤訪問、網(wǎng)絡(luò)通信。
- 不是每段代碼都需要穩(wěn)定接口,有的接口可能只是實現(xiàn)細節(jié)。但還是要停下來想一下:如果要使用多個線程實現(xiàn)這個操作,需要什么樣的接口?是否可以向量化?”
- 本條目和 Per.2 并不矛盾,而是它的補充:鼓勵開發(fā)者在必要且時機成熟時進行優(yōu)化。
移動語義
《C++ Core Guidelines 解析》針對本條目重點補充了移動語義:寫算法時,應(yīng)使用移動語義,而不是拷貝。移動語義有以下好處:
- 移動開銷比拷貝低
- 算法穩(wěn)定,因為不需要分配內(nèi)存,不會出現(xiàn)
std::bad_alloc異常 - 算法可以用于“只移類型”,如
std::unique_ptr
需要移動語義的算法遇到不支持移動操作類型,則自動“回退”到拷貝操作。
而只支持拷貝語義的算法遇到不支持拷貝操作的類型時,則編譯報錯。
Per.10 依賴靜態(tài)類型系統(tǒng)
弱類型(如 void* )、低級代碼(如把 sequence 作為單獨的字節(jié)來操作)會讓編譯器難以優(yōu)化。
《解析》中還給出了一些額外的幫助編譯器生成優(yōu)化代碼的技巧:
- 本地代碼。“本地”指在同一個編譯單元(如同一個 .c/.cpp 文件中)。例如
std::sort需要一個謂詞,傳入本地 lambda 可能會比傳入函數(shù)(指針)更快。
因為對于本地 lambda,編譯器擁有所有可用的信息來生成最優(yōu)代碼,而函數(shù)可能定義在另一個編譯單元中,編譯器無法獲取有關(guān)該函數(shù)的細節(jié),從而無法進行深度優(yōu)化。 - 簡單代碼。優(yōu)化器會搜尋可以被優(yōu)化的已知模式,簡單的代碼更容易被匹配到。如果是手寫的復(fù)雜代碼,反而可能錯失讓編譯器優(yōu)化的機會。
- 額外提示。
const、noexcept、final等關(guān)鍵字可以給編譯器提供額外的信息,有了這些額外的信息,編譯器可以大膽地做進一步優(yōu)化。當然要先搞清楚這些關(guān)鍵字的含義及產(chǎn)生的影響。
Per.11 將計算從運行時提前到編譯期
可以減少代碼尺寸和運行時間、避免數(shù)據(jù)競爭、減少運行期的錯誤處理。
constexpr
將函數(shù)聲明為 constexpr,且參數(shù)都是常量表達式,則可以在編譯期執(zhí)行。
注意:
constexpr函數(shù)可以在編譯期執(zhí)行,但不意味著只能在編譯期執(zhí)行,也可以在運行期執(zhí)行。
constexpr 函數(shù)的限制:
- 不能使用
static或thread_local變量 - 不能使用
goto - 不能使用異常
- 所有變量必須初始化為字面類型
字面類型:
- 內(nèi)置類型(及其引用)
- 有
constexpr構(gòu)造的類 - 字面類型的數(shù)組
例 1
// 舊風格:動態(tài)初始化
double square(double d) { return d*d; }
static double s2 = square(2);
// 現(xiàn)代風格:編譯期初始化
constexpr double ntimes(double d, int n) // 假設(shè) 0 <= n
{
double m = 1;
while (n--) m *= d;
return m;
}
constexpr double s3 {ntimes(2, 3)};
第一種寫法很常見,但有兩個問題:
- 運行時函數(shù)調(diào)用開銷
- 另一個線程可能在 s2 初始化之前訪問 s2
注:常量不存在數(shù)據(jù)競爭的問題
例 2
一個常用的技巧,小對象直接存在 handle 里,大對象存在堆上。
constexpr int on_stack_max = 20;
// 直接存儲
template<typename T>
struct Scoped {
T obj;
};
// 在堆上存儲
template<typename T>
struct On_heap {
T* objp;
};
template<typename T>
using Handle = typename std::conditional<
(sizeof(T) <= on_stack_max),
Scoped<T>,
On_heap<T>
>::type;
void f()
{
// double 在棧上
Handle<double> v1;
// 數(shù)組在堆上
Handle<std::array<double, 200>> v2;
}
編譯期可以計算出最佳類型,類似地技術(shù)也可用于在編譯期選擇最佳函數(shù)。
注
實際上大多數(shù)計算取決于輸入,不可能把所有的計算全部放到編譯期。除此之外,復(fù)雜的編譯期計算可能大幅增加編譯時間,并且導(dǎo)致調(diào)試困難。甚至在極少場景下,可能導(dǎo)致性能劣化。
代碼檢查建議
- 檢查是否有簡單的、可以作為(但沒有)
constexpr的函數(shù) - 檢查是否有函數(shù)的所有參數(shù)都是常量表達式
- 檢查是否有可以改為
constexpr的宏
Per.19 以可預(yù)測的方式訪問內(nèi)存
緩存對性能影響很大,一般緩存算法對相鄰數(shù)據(jù)的簡單、線性訪問效率更高。
當程序需要從內(nèi)存中讀取一個 int 時,現(xiàn)代計算機架構(gòu)會一次讀取整個緩存行(通常 64 字節(jié)),儲存在 CPU 緩存中,如果接下來要讀取的數(shù)據(jù)已經(jīng)在緩存中,則會直接使用,快很多。
例如:
int matrix[rows][cols];
// 不好
for (int c = 0; c < cols; ++c)
for (int r = 0; r < rows; ++r)
sum += matrix[r][c];
// 好
for (int r = 0; r < rows; ++r)
for (int c = 0; c < cols; ++c)
sum += matrix[r][c];
在 C++ 標準庫中,std::vector, std::array, std::string 將數(shù)據(jù)存在連續(xù)的內(nèi)存塊中的數(shù)據(jù)結(jié)構(gòu)對緩存行很友好。而 std::list 和 std::forward_list 則恰恰相反。
例如在某測試環(huán)境中,從容器中讀取并累加所有元素:
-
std::vector比std::list或std::forward_list快 30 倍 -
std::vector比std::deque快 5 倍
很多場景下,即使需要在中間插入/刪除元素,由于緩存行的原因,
std::vector的性能也可能好于std::list!
除非測量的結(jié)果表明其他容器性能好于 std::vector,否則應(yīng)將 std::vector 作為首選容器。
其他
剩下的條目截至目前還只有標題,缺少詳細描述:
- Per.12 Eliminate redundant aliases/消除冗余別名
- Per.13 Eliminate redundant indirections/消除冗余間接引用(指針解引用)
- Per.14 Minimize the number of allocations and deallocations/盡可能減少分配和釋放
- Per.15 Do not allocate on a critical branch/不在關(guān)鍵分支上分配
- Per.16 Use compact data structures/使用緊湊的數(shù)據(jù)結(jié)構(gòu):性能主要由內(nèi)存訪問決定
- Per.17 Declare the most used member of a time-critical struct first/對于時間關(guān)鍵的結(jié)構(gòu)體,把最常用的成員定義在前
- Per.18 Space is time/空間就是時間:性能主要由內(nèi)存訪問決定
- Per.30 Avoid context switches on the critical path/避免關(guān)鍵路徑上的上下文切換
總結(jié)
- 非必要,不優(yōu)化
- 先測量,再優(yōu)化
- 為編譯器優(yōu)化提供必要信息:
- 正確使用 const、final、noexcept 等關(guān)鍵字
- 為函數(shù)實現(xiàn)移動語義、如果可能,使之成為 constexpr
- 現(xiàn)代計算機架構(gòu)為連續(xù)讀取內(nèi)存而進行了優(yōu)化,應(yīng)該將
std::vector,std::array,std::string作為首選
Reference
- C++ Core Guidelines, Per: Performance
- 《性能優(yōu)化的一般策略及方法》
- 《C++ Core Guidelines 解析》
總結(jié)
以上是生活随笔為你收集整理的C++ 核心指南 —— 性能的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 论文文献书的符号
- 下一篇: java信息管理系统总结_java实现科