9 程式效率
9.1 C++語言特性的效能分級
影響軟體效能的因素眾多,包括軟體架構、運行平台(作業系統/編譯器/硬體平台)等。很多時候,程
序的效能在架構設計完成時就已經確定了。因此當一個程式的效能需要提高時,首先需要做的是用性
能偵查工具對其啟動並執行時間分布進行一個準確的測量,找出關鍵路徑和真正的瓶頸所在,然後針對瓶
頸進行分析和最佳化,而不是一味盲目地將效能低劣歸咎於所採用的語言。事實上,如果架構設計不做
修改,即使用C語言或者組合語言重新改寫,也並不能保證提高總體效能。
C++對效能的設計原則是零開銷(zero overhead),其含義是你不用為你不選擇的而付費(you don’t
pay for what you don’t use),因此需要瞭解各種C++語言特性的效能開銷。C++語言特性的效能描
述採用FREE/CHEAP/EXPENSIVE的分級。
FREE:效能開銷很小,甚至有最佳化,可以放心使用;
CHEAP:效能開銷有一定程度,多數情況下可以使用,在效能關鍵地方需要注意;
EXPENSIVE:效能開銷較大,需要按照情況使用,在效能關鍵地方需要謹慎使用。
C++語言特性 效能分級 備忘
封裝 FREE class和C的struct在使用空間上是相同的,class中的成員函
數的時間開銷也和C等效代碼是一致的。
多態 FREE 含有虛函數的class在空間上需要增加虛表指標(4位元組),在
虛函數的執行上需要間接定址的開銷。雖然有微量開銷,但
等同於C等效代碼。
名字空間 FREE Namespace會帶來符號名字串長度的增加,但C等效代碼也
需要增加前置詞字元串(比如模組名)。
隱含內聯 FREE 沒有函數調用的開銷,沒有指令跳轉的順序執行能讓編譯器
進行更好的最佳化,但會增加程式大小。
重載 FREE 等效類成員函數的開銷
構造和析構 FREE 等效類成員函數的開銷
引用 FREE 等效或優於指標的使用
引用可以避免指標的間接定址開銷。
模板 FREE~ 模板具有靜態多態的優點,部分邏輯提前到編譯階段以提升
EXPENSIVE 效能;但會導致代碼膨脹,程式尺寸增大。
RTTI CHEAP~ 執行時間有一定耗時,gcc/VC中dynamic_cast的開銷小於10
EXPENSIVE 倍函數調用,程式尺寸增大。
異常 EXPENSIVE 異常捕捉很耗時,其包括棧展開等操作,gcc/VC中異常處理
開銷大於300倍函數調用。
STL CHEAP~ STL提供線性(list)、對數級(map)和常量級(hash_map)不同
EXPENSIVE 效能的容器,建議根據應用實際需求選用。
更多的資訊請參見延伸閱讀材料:
1、《Technical Report on C++ Performance》ISO/IEC PDTR 18015,2003-08-11
2、《The Inefficiency of C++, Fact or Fiction?》Anders Lundgren, IAR Systems
3、《提高C++效能的編程技術》,布爾卡(Dov Bulka),梅休(David Mayhew) 著。
9.2 C++語言的效能最佳化指導
原則9.1 先測量再最佳化,避免不成熟的最佳化
說明:效能涉及到非常多因素,目標不明確的語言層面最佳化很難顯著地改善效能。建議首先測量到性
能瓶頸,再對這些效能瓶頸進行針對性的最佳化。初學者常犯的一個錯誤,就是編寫代碼時著迷於過度
最佳化,犧牲了代碼的可理解性。
原則9.2 選用合適的演算法和資料結構
說明:代碼最佳化應該從選擇合適的演算法和資料結構開始。對於元素個數超過1000的資料,其順序尋找
演算法效率要遠低於對數效能的尋找演算法。std::vector<int>的佔用空間約是N*sizeof(int),而
std::list<int>的佔用空間約是3N*sizeof(int)。std::map提供了對數級的尋找演算法,而hash_map則
更高,提供了常數級的尋找演算法,但hash_map需要選擇合適的hash演算法和桶大小。
建議9.1 在建構函式中用初始化代替賦值
說明:通過成員初始化列表來進行初始化總是合法的,效率也高於在建構函式體內賦值。
樣本:
class A
{
string s1_;
public:
A(){s1_ = "Hello,world "; }
};
實際上,產生的建構函式代碼類似如下:
A():s1_(){s1 = "Hello, world"; }
成員s1_的預設建構函式已被隱式調用,建構函式體中的初始化實際上上在調用operator=
而初始化列表只需調用一次s1_的建構函式,相對效率更高。
A():s1_("Hello,world "){}
建議9.2 當心空的建構函式或解構函式的開銷
說明:空建構函式的開銷不一定是0,空建構函式也包括基類構造、類內部成員對象的構造等。如果對
象的建構函式或解構函式有相當的開銷,建議避免臨時對象的使用,並在效能關鍵路徑上考慮避免非
臨時對象的構造和析構,比如Lazy/Eager/Partial Acquisition設計模式。
class Y
{
C c;
D d;
};
class Z : public Y
{
E e;
F f;
public:
Z() { };
};
Z z; //initialization of c, d, e, f
建議9.3 對象參數盡量傳遞引用(優先)或指標而不是傳值
說明:對於數實值型別的int、char等傳值既安全又簡單;但對於自訂的class、struct、union等對象
來說,傳引用效率更高:
不需要拷貝。class等對象的尺寸一般都大於引用,尤其可能包含隱式的資料成員,虛函數指標
等,所以傳值的拷貝的代價遠遠大於引用。
不需要構造和析構。如果傳值,傳入是調用拷貝建構函式,函數退出時還要析構。
有利於函數支援衍生類別。
之所以首選引用,見建議3.2。
樣本:
void f(T x) //bad
void f(T const* x) //good
void f(T const& x) //good, prefer
建議9.4 盡量減少臨時對象
說明:臨時對象的產生條件包括:運算式、函數返回、預設參數以及尾碼++等等,臨時對象都需要創
建和刪除。對於那些非小型的對象,建立和刪除在處理時間和記憶體方面的代價不菲。採用如下方法可
以減少臨時對象的產生。
用引用或指標類型的參數代替直接傳值;
用諸如+=代替+
使用匿名的臨時對象;
避免隱式轉換;
樣本:
Matrix a;
a = b + c; //bad: (b + c) creates a temporary
Matrix a = b;
a += c; //good: no temporary objects created
建議9.5 優先採用前置自增/自減
說明:後置++/--是將對象拷貝到臨時對象中,自身++/--,然後再返回臨時對象。此過程在非簡單數
據類型時較為耗時(單一資料型別編譯器可以最佳化)
樣本:
for (list<X>::iterator it = mylist.begin();
it != mylist.end();
++it) //good: rather than it++
{
//...
}
建議9.6 簡單存取方法盡量採用內嵌函式
說明:小函數頻繁跳轉會帶來效能上的損失,內聯可以避免效能損失。
class X
{
private:
int value_;
double* array_;
size_t size_;
public:
inline int value() { return value_; }
inline size_t size() { return size_; }
};
建議9.7 要審視標準庫的效能規格
說明:std::string是個巨大類,若使用不當(比如大量的+操作)會導致效能迅速下降;
std::list<T>::size()在某些實現版本中是線性,所以在if(myList.size()==0)時可以考慮用
if(myList.empty())替換;標準輸入輸出是效能瓶頸,如果不混用C++和C的標準輸入輸出庫,可以考
慮關掉同步:std::ios_base::sync_with_stdio(false)。
建議9.8 用對象池重載動態記憶體管理器
說明:系統調用new和delete涉及到系統調用等複雜處理,時間和空間開銷都較大,對於DOPRA的記憶體
申請機制也有類似的情況。建議對於特定類的申請和釋放,採用自訂的對象池機制管理該對象的申
請和釋放,而不用作業系統或DOPRA的記憶體管理機制。對象池的大小需要預先確定或是採用類似
std::vector.resize()的方法可以動態增長。
建議9.9 注意大尺寸數組的初始化效率
說明:我們常這麼初始化數組:
char szAccount[MAX_ACCOUNT_LEN] = {0};//數組大小16
對於字串確保有結束符即可,故要初始化也應該用“szAccount[0] = 0;”替換之,當然在非關鍵路
徑,上述的一行程式碼完成初始化代碼更簡潔,也能夠被接受。
char chTempBuff[MAX_MSG_LEN] = {0};//數組大小為40K
而對於如此大的一塊記憶體清0,應該用memset等庫函數替代之。編譯器不最佳化情況下,對於{0}的初始
化,通常是一個位元組逐一賦值為0,而memset在64位平台下很可能是一次8個位元組。故當數組大於10個
位元組時,兩者的效能差距在2~8倍,根據數組大小而定。
特別是產生或者刪除大尺寸的對象數組,每個數群組成員的建構函式或解構函式度要被調用一次,花費
的時間更多。
建議9.10 避免在函數內部的小塊記憶體配置
例子 某函數內部,根據訊息長度分配記憶體,然後做相關操作,函數退出前釋放記憶體,如下:
char *pMsg = new char[msgLen];
其實pMsg的生命週期僅僅在函數內,它所指向的記憶體其實應該是一個臨時變數,如果我們能夠預測其
最大長度遠小於線程棧空間,比如最大幾十K,或者只有幾十位元組,那麼就應該聲明一個足夠大的臨時
數組,如下:
assert(msgLen <= MAX_MSG_LEN);//某些情況下可能需要if檢測,而斷言檢測可能不充分。
char chMsg[MAX_MSG_LEN];//不會有失敗和釋放的處理,效率也完全不在一個數量級