提高C++性能的编程技术笔记:编码优化+测试代码
緩存:在現代處理器中,緩存經常與處理器中的數據緩存和指令緩存聯系在一起。緩存主要用來存儲使用頻繁而且代價高昂的計算結果,這樣就可以避免對這些結果的重復計算。如,循環內對常量表達式求值是一種常見的低性能問題。
預先計算:預先計算和緩存聯系緊密。當緩存某個計算的結果時,需要付出的代價是在對性能有重大影響的關鍵路徑上完成一次計算。如果采用預先計算,那么甚至連這一次計算也可免了。將預先計算放置在影響性能的關鍵路徑之外(例如初始化階段),就可以避免在性能關鍵路徑上進行代價高昂的計算。
降低靈活性:使用堆存儲空間(頻繁的new/delete)存放IP地址可以改成使用固定大小的局部變量數組代替。
80-20法則:加快常用路徑的速度:80-20法則適用于很多場合:80%的程序執行只遍歷20%代碼,80%的程序運行時間耗費在執行路徑的所遇到的20%函數上。80-20法則有力地證明了過于草率的判斷是一種錯誤這個觀點。
延遲計算:為一個最終可能不需要的計算付出性能代價顯然不是明智之舉。然而在復雜的代碼中這種情況比比皆是。我們不應該執行”只在某種情況下”才需要的昂貴計算,而應該只在必要的時候執行昂貴計算。這通常意味著把計算推遲到真正需要的時候才進行,因此稱之為延遲計算(Lazy Evaluation)。在C++中,對象的定義會調用構造函數和析構函數,這可能是高成本的,因為它導致了立即計算----而這正是我們所要避免的。延遲計算原則建議我們推遲對象的定義,直到要使用該對象時再定義。為不一定用到的構造函數和析構函數付出代價是沒有意義的。不僅應該將對象的創建推遲至合適的位置,而且應該直到具備了一個有效創建操作所必需的全部條件后,再創建對象。
無用計算:延遲計算是指那些不總是必須執行的計算,至于哪些計算是必須執行的與程序的執行流程有關,而無用計算是指那些根本無須執行的計算。無論執行流程如何,這些計算結果從不使用,因此它們是完全沒有意義的。盡量使用初始化列表方式完成類成員的賦值。
系統體系結構:內存訪問的代價差別很大。在某個特定的RISC體系結構中,若數據位于數據緩存中,那么訪問它需要耗費一個CPU周期;若位于主存中(緩存失敗),則需要8個CPU周期;若位于硬盤上(頁面錯誤),則需要400000個CPU周期。雖然具體的數值會發生變化,但對于不同的處理器體系結構,周期數在總的關系上是一致的:緩存成功、緩存失敗和頁面錯誤之間的速度相差多個數量級。
當訪問數據時,最先搜索的是數據緩存。若數據不在緩存中,硬件產生緩存失敗信號,該信號會從RAM或硬盤加載數據至緩存中。緩存以緩存行為單位,通常加載比我們所尋找的特定數據項更大的一塊數據。
內存管理:動態分配和釋放堆內存的代價比較昂貴。從性能角度來講,使用不需要顯式管理的內存所產生的代價要低得多。被定義成局部變量的對象存放于堆棧上。該對象所占用的堆棧空間是為相應函數預留的堆棧空間的一部分,該對象被定義在這個函數范圍內。
編譯器優化:一個優秀的編譯器可以代替開發者實施一些重要的優化,而無須開發者對源代碼進行任何干預。第一個想到的優化是寄存器分配。當變量位于寄存器中時,載入和存儲該變量是最快的。第二個需要特別注意的優化就會內聯。
通常情況下,編譯器默認根本不會進行任何優化。這意味著這些重要的性能優化將不會生效----即使在代碼中使用了關鍵字register和inline也無濟于事。編譯器會自動忽略這些關鍵字,而且它經常這樣做。為了更好地利用這些優化手段,必須通過向命令行添加開關或者在GUI界面上選擇性能優化選項,手工打開編譯器優化。
編碼優化在范圍上是局部的,并且不需要對程序的整體設計有深入的理解。當你加入到一個正在進行的開發項目中,并且你對其設計還沒有完全理解時,這會是一個很好的起點。
最快的代碼是從不執行的代碼。試著按照以下步驟去剔除那些代價高昂的計算:
(1). 你打算使用該計算結果嗎?聽起來有點可笑,但這種可笑的事確實會發生----有時我們執行了計算但從未使用計算的結果。
(2). 你現在需要該結果嗎?請在真正需要的時候再進行計算。在一些執行流程中有些結果永遠不會被使用,因此不必過早地計算。
(3). 你是否已經知道結果?如果在程序執行流程的前期已經計算出了結果,那么應該使用該結果成為可重用的。
有的時候可能無法繞開該計算,此時就必須完成它。那么現在的挑戰就是加快計算速度:
(1). 該計算是否過于通用?你的實現只需要跟該領域要求的一樣靈活就行,而無須奢求。可以充分利用簡化的假設以降低靈活性來增加速度。
(2). 一些靈活性隱藏在函數調用中。通過實現庫調用的自定義版本可以提升速度。不過,這些庫調用必須是被頻繁調用的,否則你的努力將得不到明顯效果。熟悉你所使用的庫和系統調用中隱藏的代價。
(3). 盡量減少內存管理調用的數量。在絕大多數編譯器中,這些調用的代價都是非常高的。
(4). 如果考慮所有可能的輸入數據,則可以發現20%的數據在80%時間里出現。因此,應當以犧牲其它不經常出現的場景為代價來提高典型輸入的處理速度。
(5). 緩存、RAM和磁盤訪問的速度差異很明顯。應該多編寫緩存友好的代碼。
以下是測試代碼(coding_optimizations.cpp):
#include "coding_optimizations.hpp"
#include <ctype.h>
#include <string.h>
#include <iostream>
#include <chrono>
#include <string>namespace coding_optimizations_ {// reference: 《提高C++性能的編程技術》:第十三章:編碼優化namespace {
static char uppercaseTable[256];void initLookupTable()
{for (int i = 0; i < 256; ++i) {uppercaseTable[i] = toupper(i);}
}class Student1 {
public:// C++保證在Student1的構造函數體執行之前,所有的成員對象已經創建完成,此處即string型的name對象。// 既然我們沒有顯示告訴編譯器如何構造它,編譯器就插入了對string默認構造函數的調用。該調用在Student1的構造函數體執行之前進行。Student1(char* nm) { name = nm; }
private:std::string name;
};class Student2 {
public:// 通過在Student2的構造函數初始化列表中顯示指明string構造函數,可以避免Student1中的無效計算// 由于我們明確告訴編譯器使用哪個string構造函數,編譯器將不再隱式地調用string默認構造函數。因此我們實現了一步完成string成員對象的構造Student2(char* nm) : name(nm) {}
private:std::string name;
};} // namespaceint test_coding_optimizations_1()
{initLookupTable();std::chrono::high_resolution_clock::time_point time_start, time_end;const int count{100000000000}, count2{100000};const char* header{"afaIELQEadsfjl943082jdfaadfajqwppreijfadfadfaoueheufiekasdLdamsaldfadfawweevKKA"};int length = strlen(header);char ch;{ // test lowercase letter to uppercase letter: normaltime_start = std::chrono::high_resolution_clock::now();for (int t = 0; t < count; ++t) {for (int i = 0; i < count; ++i) {char* p = const_cast<char*>(header);for (int j = 0; j < length; ++j) {ch = toupper(*p++);//fprintf(stdout, "%c", ch); }//fprintf(stdout, "\n");}}time_end = std::chrono::high_resolution_clock::now(); fprintf(stdout, "lowercase letter to uppercase letter normal time spend: %f seconds\n",(std::chrono::duration_cast<std::chrono::duration<double>>(time_end-time_start)).count());
}{ // test lowercase letter to uppercase letter: pre-calculatedtime_start = std::chrono::high_resolution_clock::now();for (int t = 0; t < count; ++t) {for (int i = 0; i < count; ++i) {char* p = const_cast<char*>(header);for (int j = 0; j < length; ++j) {ch = uppercaseTable[*p++];//fprintf(stdout, "%c", ch); }//fprintf(stdout, "\n");}}time_end = std::chrono::high_resolution_clock::now(); fprintf(stdout, "lowercase letter to uppercase letter pre-calculated time spend: %f seconds\n",(std::chrono::duration_cast<std::chrono::duration<double>>(time_end-time_start)).count());
}{ // test unuseful calculate: normaltime_start = std::chrono::high_resolution_clock::now();for (int t = 0; t < count2; ++t) {Student1 st("beijing");}time_end = std::chrono::high_resolution_clock::now(); fprintf(stdout, "unuseful calculate normal time spend: %f seconds\n",(std::chrono::duration_cast<std::chrono::duration<double>>(time_end-time_start)).count());
}{ // test unuseful calculate: list inittime_start = std::chrono::high_resolution_clock::now();for (int t = 0; t < count2; ++t) {Student2 st("beijing");}time_end = std::chrono::high_resolution_clock::now(); fprintf(stdout, "unuseful calculate list init time spend: %f seconds\n",(std::chrono::duration_cast<std::chrono::duration<double>>(time_end-time_start)).count());
}return 0;
}} // namespace coding_optimizations_
執行結果如下:
GitHub:?https://github.com/fengbingchun/Messy_Test?
總結
以上是生活随笔為你收集整理的提高C++性能的编程技术笔记:编码优化+测试代码的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 提高C++性能的编程技术笔记:引用计数+
- 下一篇: 以安装PyTorch为例说明Anacon