C++ 程式設計/最佳化
最佳化可以被認為是提高某事物效能的定向努力,這是工程學中的一個重要概念,特別是我們正在討論的軟體工程領域。我們將處理特定的計算任務和最佳實踐,以減少資源利用率,不僅是系統資源,還有程式設計師和使用者的資源,所有這些都是基於從假設和邏輯步驟的經驗驗證中發展而來的最優解。
所有采取的最佳化步驟都應以減少需求和促程序序目標為目標。任何主張只能透過對給定問題和應用的解決方案進行效能分析來證實。沒有效能分析,任何最佳化都是無稽之談。
最佳化通常是程式設計師之間討論的話題,並非所有結論都是一致的,因為它們與目標、程式設計師經驗密切相關,並取決於特定的設定。最佳化級別主要直接取決於程式設計師採取的行動和做出的決定。這些可能是簡單的事情,從基本的編碼實踐到選擇用來建立程式的工具。甚至選擇正確的編譯器也會產生影響。一個好的最佳化編譯器允許程式設計師定義他對最佳化結果的期望;編譯器最佳化的好壞取決於程式設計師對編譯結果的滿意程度。
最安全的一種最佳化方法是減少複雜性,簡化組織和結構,同時避免程式碼膨脹。這需要規劃的能力,而不失去對未來需求的追蹤,事實上,這是程式設計師在眾多因素之間做出的妥協。
程式碼最佳化技術可分為以下幾類
- 高階最佳化
- 演算法最佳化(數學分析)
- 簡化
- 低階最佳化
- 迴圈展開
- 強度削弱
- Duff's Device
- 乾淨迴圈
“保持簡單,愚蠢”(KISS)原則要求在開發中優先考慮簡單性。這與阿爾伯特·愛因斯坦的一句格言非常相似,即“一切都應該儘可能簡單,但不要更簡單”,對於許多采用者來說,難點在於確定應保持何種程度的簡單性。無論如何,對基本和更簡單系統的分析總是更容易,消除複雜性也將為程式碼重用和更通用的任務和問題解決方法開啟大門。
程式碼清理的大部分好處對於經驗豐富的程式設計師來說應該是顯而易見的,由於採用了良好的程式設計風格指南,它們已經成為第二天性。但就像任何人類活動一樣,錯誤會發生,也會做出例外,因此,在本節中,我們將嘗試記住那些可以對程式碼最佳化產生影響的微小變化。
- 使用虛成員函式
記住虛成員函式對效能的影響(在介紹virtual 關鍵字時已經討論過)。當最佳化成為一個問題時,大多數與最佳化相關的專案設計更改將不再可能,但仍然可以清理一些遺留問題。確保沒有多餘的虛擬函式使用(例如,在你的類/結構繼承樹的葉子節點中),將允許進行其他最佳化(即:編譯器最佳化內聯)。
當今系統中的主要瓶頸之一是處理記憶體快取,無論是CPU 快取還是物理記憶體資源,即使分頁問題正變得越來越少見。由於程式將在設計級別處理的資料(以及負載級別)是高度可預測的,因此更好的最佳化仍然落到程式設計師身上。
應該將適當的資料結構儲存在適當的容器中,優先儲存指向物件的指標而不是物件本身,使用“智慧”指標(參見 Boost 庫),並且不要嘗試將 auto_ptr<> 儲存在 STL 容器中,這是標準不允許的,但有些實現已知會錯誤地允許它。
避免在容器中間刪除和插入元素,在容器末尾進行操作開銷更小。當物件的數目未知時,使用 STL 容器;當物件數目已知時,使用靜態陣列或緩衝區。這需要理解每個容器及其 O(x) 保證。
以 STL 容器為例,在使用 (myContainer.empty()) 與 (myContainer.size() == 0) 之間的區別時,重要的是要理解,根據容器型別或其實現,size 成員函式可能需要先計算物件數量,然後再將其與零進行比較。這在列表型別容器中非常常見。
雖然 STL 試圖為一般情況提供最優解,但如果效能不符合你的要求,請考慮為你的情況編寫你自己的最優解,也許是一個自定義容器(可能基於 vector),它不呼叫單個物件解構函式,並使用自定義分配器來避免刪除時的開銷。
使用記憶體預分配可以提高一些速度,並且像使用 STL vector<T>::reserve() 一樣簡單。最佳化系統記憶體和目標硬體的使用。在當今的系統中,有虛擬記憶體、執行緒和多核(每個核都有自己的快取),其中主記憶體上的 I/O 操作以及在記憶體中移動資料的所需時間會減慢速度。這可能成為效能瓶頸。相反,選擇基於陣列的資料結構(快取一致資料結構),例如 STL vector,因為資料是連續儲存在記憶體中的,而不是指標連結的資料結構,例如連結串列。這將避免“因交換而死”,因為程式需要訪問高度碎片化的資料,甚至有助於大多數現代處理器今天執行的記憶體預取。
儘可能避免按值返回容器,按引用傳遞容器。
安全總是有成本,即使在程式設計中也是如此。對於任何演算法,新增檢查都會導致完成所需的步驟數量增加。隨著語言變得更加複雜和抽象,理解所有細微差別(並記住它們)會增加獲得所需經驗所需的時間。不幸的是,C++ 語言一些實現者採取的大多數步驟都缺乏程式設計師的可見性,並且由於它們超出了標準語言,因此通常不會被學習。請務必熟悉正在使用的 C++ 實現的任何擴充套件或特殊情況。
作為一種將決策權賦予程式設計師的語言,C++ 提供了許多例項,其中可以以類似但不同的方式實現類似的結果。理解這些有時細微的差異很重要。例如,在決定訪問 std::vector 成員所需的條件時,可以選擇 []、at() 或迭代器。所有這些都有類似的結果,但具有不同的效能成本和安全考慮因素。
最佳化也反映在程式碼的有效性上。如果你可以使用一個已經存在的程式碼庫/框架,該框架可供相當數量的程式設計師訪問,那麼你可以預期它將包含更少的錯誤,並且針對你的特定需求進行了最佳化。
一些程式碼庫可以作為庫供程式設計師使用。請謹慎考慮依賴關係,並檢查實現方式:如果在沒有考慮的情況下使用,也可能導致程式碼膨脹和記憶體佔用增加,以及降低程式碼的可移植性。我們將在本書的庫部分中詳細介紹這些庫。
為了提高程式碼重用率,你可能會將程式碼分割成更小的部分,檔案或程式碼,請記住,更多檔案和總體複雜性也會增加編譯時間。
在建立函式或演算法來解決特定問題時,我們有時會處理數學結構,這些結構專門指示透過已建立的數學最小化方法進行最佳化,這屬於工程分析最佳化的特定領域。
如前所述,在檢查 inline 關鍵字時,它允許定義一種內聯型別的函式,其工作方式類似於 迴圈展開,以提高程式碼效能。非行內函數需要一個呼叫指令,幾個指令來建立堆疊幀,然後還需要幾個指令來銷燬堆疊幀並從函式返回。透過複製函式體而不是進行呼叫,機器程式碼的大小會增加,但執行時間會減少。
除了使用 inline 關鍵字來宣告行內函數外,最佳化編譯器還可以決定將其他函式也內聯(參見編譯器最佳化部分)。
如果可移植性不是問題,並且你精通匯編語言,你可以使用它來最佳化計算瓶頸,即使檢視反彙編器的輸出通常也有助於尋找改進它的方法。在你的程式碼中使用匯編會帶來一些其他問題(例如可維護性),所以請在你的最佳化過程中將其作為最後的手段使用,如果你使用它,請確保將你的操作記錄清楚。
x86 反彙編 華夏公益教科書提供了一些使用 x86 彙編程式碼的最佳化示例。
一些專案可能需要很長時間才能編譯。要減少完成編譯所需的時間,第一步是檢查是否存在任何硬體缺陷。你的記憶體資源可能不足,或者你的 CPU 速度可能很慢,即使你的硬碟存在大量碎片也會增加編譯時間。
另一方面,問題可能不是由於硬體限制,而是由於你使用的工具造成的,請檢查你是否在使用適合當前任務的工具,檢視你是否擁有最新版本,或者如果有,是否是它導致了問題,一些不相容性可能是由於更新造成的。在編譯器中,更新總是更好的,但你應該先檢查一下發生了哪些變化以及它是否符合你的目的。
經驗表明,如果你遇到編譯速度慢的問題,你嘗試編譯的程式可能是設計不當的,請檢查物件依賴關係的結構,包括檔案,並花一些時間來構建自己的程式碼,以儘量減少更改後的重新編譯,如果編譯時間證明這一點是合理的。
使用預編譯標頭檔案和外部標頭檔案保護,這將減少編譯器的工作量。
編譯器最佳化 是調整編譯器輸出的過程,主要透過自動方式,以嘗試改程序序員請求的操作,從而最大程度地減少或最大化已編譯程式的某些屬性,同時確保結果完全相同。透過利用編譯器最佳化,程式設計師可以編寫更直觀的程式碼,並仍然以合理的速度執行它們,例如,跳過使用前置自增/自減運算子。
一般來說,最佳化並沒有,也不能在 C++ 標準中定義。標準設定了規則和最佳實踐,這些規則和最佳實踐規定了輸入和輸出的規範化。C++ 標準本身允許編譯器在執行任務時有一定的自由度,因為某些部分被標記為實現相關,但通常會建立一個基線,即使如此,一些供應商/實現者也會在其中加入一些獨特的特性,顯然是為了安全和最佳化的好處。
需要牢記的一點是,沒有完美的 C++ 編譯器,但大多數最新的編譯器預設情況下會執行一些簡單的最佳化,這些最佳化試圖抽象並利用現有的更深層的硬體最佳化或目標平臺的特定特性,大多數這些最佳化幾乎總是受歡迎的,但仍然取決於程式設計師對正在發生的事情以及它們是否確實有益有一個想法。因此,強烈建議檢查你的編譯器文件,瞭解它的操作方式以及哪些最佳化在程式設計師的控制之下,僅僅因為編譯器理論上可以進行某些最佳化,並不意味著它會這樣做,甚至不意味著它會帶來最佳化效果。
程式設計師可用的最常見的編譯器最佳化選項分為三類
- 速度;提高生成的目的碼的執行時效能。這是最常見的最佳化
- 空間;減小生成的目的碼的大小
- 安全性;減少資料結構被破壞的可能性(例如,確保不會寫入非法陣列元素)
不幸的是,許多“速度”最佳化會使程式碼變大,而許多“空間”最佳化會使程式碼變慢,這就是所謂的時空權衡。
自動內聯類似於隱式內聯。內聯可以是最佳化,也可以是弊端,具體取決於程式碼和選擇的最佳化選項。
如前所述,執行時間是指程式執行的持續時間,從開始到結束。這是分配所有執行已編譯程式碼所需資源並有望釋放資源的地方,這是任何要執行的程式的最終目標,因此它應該是最終最佳化的目標。
過去,計算機記憶體價格昂貴,技術上尺寸有限,是程式設計師的稀缺資源。程式設計師們花費了大量的智慧來實現複雜的程式,並使用盡可能少的資源來處理大量資料。如今,現代系統擁有足夠多的記憶體來滿足大多數用途,但容量需求和期望也隨之增長;因此,減少記憶體使用的技術仍然至關重要,事實上,隨著移動計算的重要性不斷提升,執行效能也獲得了新的動力。
衡量程式的記憶體使用情況既困難又耗時,而且程式越複雜,獲得良好指標就越困難。問題的另一方面是,除了最基本和通用的考慮之外,沒有標準的基準測試(並非所有記憶體使用都相同)或實踐來處理這個問題。
- 請記住對
std::vector(或 deque)使用swap()。
當嘗試使用 swap() 減少(或歸零)向量或 deque 的大小,在該型別的標準容器上,將保證釋放記憶體,並且不會使用用於增長的開銷緩衝區。它還會避免使用 erase() 或 reserve() 的謬誤,這些操作不會減少記憶體佔用。
始終需要在系統性能和資源消耗之間保持平衡。延遲例項化是一種記憶體節省機制,透過該機制,物件初始化被推遲到需要時才進行。
請檢視以下示例
#include <iostream>
class Wheel {
int speed;
public:
int getSpeed(){
return speed;
}
void setSpeed(int speed){
this->speed = speed;
}
};
class Car{
private:
Wheel wheel;
public:
int getCarSpeed(){
return wheel.getSpeed();
}
char const* getName(){
return "My Car is a Super fast car";
}
};
int main(){
Car myCar;
std::cout << myCar.getName() << std::endl;
}
預設情況下,類 Car 的例項化會例項化類 Wheel。整個類的目的是列印汽車的名稱。由於例項化的 Wheel 沒有任何用途,初始化它完全是資源浪費。
最好推遲不需要的類的例項化,直到需要它為止。修改上面的類 Car 如下所示
class Car{
private:
Wheel *wheel;
public:
Car() {
wheel=NULL; // a better place would be in the class constructor initialization list
}
~Car() {
delete wheel;
}
int getCarSpeed(){
if (wheel == NULL) {
wheel = new Wheel();
}
return wheel->getSpeed();
}
char const* getName(){
return "My Car is a Super fast car";
}
};
現在,只有當呼叫成員函式 getCarSpeed() 時才會例項化 Wheel。
正如在檢查執行緒時所看到的,執行緒可以是一種“簡單”的形式,可以利用硬體資源並最佳化程式的速度效能。在處理執行緒時,您應該記住,執行緒在複雜性、記憶體方面會帶來成本,如果在需要同步時處理不當,甚至會降低速度效能,如果設計允許,最好讓執行緒儘可能地無障礙執行。

效能分析是一種動態程式分析(與靜態程式碼分析相對),包括使用在程式執行時收集的資訊來研究程式的行為。它的目的通常是確定程式的哪些部分需要最佳化。主要透過確定程式的哪些部分佔用了大部分執行時間,導致訪問資源的瓶頸或訪問這些資源的級別。
比較應用程式效能時,全域性時鐘執行時間應該是底線。透過檢查執行的漸近階數來選擇演算法,因為在並行設定中,它們將繼續提供最佳效能。如果您發現一個無法並行化的熱點,即使在檢查呼叫堆疊的更高級別之後也是如此,那麼您應該嘗試找到一個更慢但可並行化的演算法。
- 分支預測效能分析器
- 生成呼叫圖的快取效能分析器
- 逐行效能分析
- 堆效能分析器
- 免費效能分析工具
- Valgrind(http://valgrind.org/)用於構建動態分析工具的工具框架。包括快取和分支預測效能分析器、生成呼叫圖的快取效能分析器和堆效能分析器。它在以下平臺上執行:X86/Linux、AMD64/Linux、PPC32/Linux、PPC64/Linux 和 X86/Darwin(Mac OS X)。根據 GNU 通用公共許可證版本 2 開放原始碼。
- GNU gprof(http://www.gnu.org/software/binutils/)是一個性能分析工具。該程式最初是在 1982 年的 SIGPLAN 編譯器構造研討會上介紹的,現在是大多數 UNIX 版本中提供的 binutils 的一部分。它能夠監控函式(甚至原始碼行)中花費的時間以及對它們的呼叫。根據 GNU 通用公共許可證開放原始碼。
- Linux perf(http://perf.wiki.kernel.org/)是一個性能分析工具,它是 Linux 核心的一部分。它透過取樣執行。
- WonderLeak 是一款高效能 Windows 堆和控制代碼分配效能分析器,適用於使用 C/C++ API 和 CLI 整合的 x86/x64 本機程式碼開發人員。
- 商業效能分析工具
- Deleaker(http://deleaker.com/)是一個工具和 Visual Studio 擴充套件,用於查詢記憶體洩漏、控制代碼、GDI 和 USER 物件洩漏。適用於 Windows,支援 x86 / x64。它基於鉤子,不需要程式碼檢測。
