C++ 的效率淺析
自從七十年代C語言誕生以來,一直以其靈活性、高效率和可移植性為軟體開發人員
所鐘愛,成為系統軟體開發的首選工具。而 C++ 作為 C 語言的繼承和發展,不僅保留了
C 語言的高度靈活、高效率和易于了解等諸多優點,還包含了幾乎所有面向對象的特征,
成為新一代軟體系統建構的利器。
相對來說,C 語言是一種簡潔的語言,所涉及的概念和元素比較少,主要是:宏
(macro)、指針(pointer)、結構(struct)、函數(function)和數組(array),比較容易掌
握和了解。而 C++ 不僅包含了上面所提到的元素,還提供了私有成員(private
members)、公有成員(public members)、函數重載(function overloading)、預設參數
(default parameters)、構造函數、析構函數、對象的引用(references)、操作符重載
(operator overloading)、友元(friends)、模闆(templates)、異常處理(exceptions)等
諸多的要素,給程式員提供了更大的設計空間,同時也增加了軟體設計的難度。
C 語言之是以能被廣泛的應用,其高效率是一個不可忽略的原因,C 語言的效率能達
到彙編語言的 80% 以上,對于一種進階語言來說,C 語言的高效率就不言而喻了。那
麼,C++ 相對于C來說,其效率如何呢?實際上,C++ 的設計者 Stroustrup 要求 C++
效率必須至少維持在與 C 相差 5% 以内,是以,經過精心設計和實作的 C++ 同樣有很高
的效率,但并非所有 C++ 程式具有當然的高效率,由于 C++ 的特殊性,一些不好的設計
和實作習慣依然會對系統的效率造成較大的影響。同時,也由于有一部分程式員對 C++
的一些底層實作機制不夠了解,就不能從原理上了解如何提高軟體系統的效率。
本文主要讨論兩個方面的問題:第一,對比 C++ 的函數調用和C函數調用,解析 C++
的函數調用機制;第二,例舉一些 C++ 程式員不太注意的技術細節,解釋如何提高 C++
的效率。為友善起見,本文的讨論以下面所描述的單一繼承為例(多重繼承有其特殊性,
另作讨論)。
class X
{
public:
virtual ~X(); // 析構函數
virtual void VirtualFunc(); // 虛函數
inline int InlineFunc() { return m_iMember}; // 内聯函數
void NormalFunc(); // 普通成員函數
static void StaticFunc(); // 靜态函數
private:
int m_iMember;
};
class XX: public X
{
public: XX();
virtual ~XX();
virtual void VirtualFunc();
private:
String m_strName;
int m_iMember2;
};
C++ 的的函數分為四種:内聯函數(inline member function)、靜态成員函數
(static member function)、虛函數(virtual member function)和普通成員函數。
内聯函數類似于 C 語言中的宏定義函數調用,C++ 編譯器将内聯函數的函數體擴充
在函數調用的位置,使内聯函數看起來象函數,卻不需要承受函數調用的開銷,對于一些
函數體比較簡單的内聯函數來說,可以大大提高内聯函數的調用效率。但内聯函數并非沒
有代價,如果内聯函數體比較大,内聯函數的擴充将大大增加目标檔案和可運作檔案的大
小;另外,inline 關鍵字對編譯器隻是一種提示,并非一個強制指令,也就是說,編譯
器可能會忽略某些 inline 關鍵字,如果被忽略,内聯函數将被當作普通的函數調用,編
譯器一般會忽略一些複雜的内聯函數,如函數體中有複雜語句,包括循環語句、遞歸調用
等。是以,内聯函數的函數體定義要簡單,否則在效率上會得不償失。
靜态函數的調用,如下面的幾種方式:
X obj; X* ptr = &obj;
obj.StaticFunc();
ptr->StaticFunc();
X::StaticFunc();
将被編譯器轉化為一般的 C 函數調用形式,如同這樣:
mangled_name_of_X_StaticFunc();
//obj.StaticFunc();
mangled_name_of_X_StaticFunc();
// ptr->StaticFunc();
mangled_name_of_X_StaticFunc();
// X::StaticFunc();
mangled_name_of_X_StaticFunc() 是指編譯器将 X::StaticFunc() 函數經過變形
(mangled)後的内部名稱(C++ 編譯器保證每個函數将被 mangled 為獨一無二的名稱,不
同的編譯器有不同的算法,C++ 标準并沒有規定統一的算法,是以 mangled 之後的名稱
也可能不同)。可以看出,靜态函數的調用同普通的 C 函數調用有完全相同的效率,并沒
有額外的開銷。
普通成員函數的調用,如下列方式:
X obj; X *ptr = &obj;
obj.NormalFunc();
ptr->NormalFunc();
将被被編譯器轉化為如下的C函數調用形式,如同這樣。
mangled_name_of_X_NormalFunc(&obj);
//obj.NormalFunc();
mangled_name_of_X_NormalFunc(ptr);
// ptr->NormalFunc();
可以看出普通成員函數的調用同普通的C調用沒有大的差別,效率與靜态函數也相
同。編譯器将重新改寫函數的定義,增加一個const X *this參數将調用對象的位址傳送
進函數。
虛函數的調用稍微複雜一些,為了支援多态性,實作運作時刻綁定,編譯器需要在每
個對象上增加一個字段也就是 vptr 以指向類的虛函數表 vtbl,如類 X 的對象模型如下
圖所示(本文中對此不多做解釋,若想進一步了解,可以參考其它材料)。
+---------------+ +---------------------------+ +-------------------+
| int m_iMember | /| Type_info of Class I 指針 |-->| |
+---------------+ / +---------------------------+ +-------------------+
| vptr |/ | I::~I() 函數位址 | 類 I 的類型資訊表
+---------------+ +---------------------------+
I 對象記憶體模型 | I::VirtualFunc() 函數位址 |
+---------------------------+
類 I 的虛函數表
虛函數的多态性隻能通過對象指針或對象的引用調用來實作,如下的調用:
X obj;
X *ptr = &obj; X &ref = obj;
ptr->VirtualFunc();
ref.VirtualFunc();
将被C++編譯器轉換為如下的形式。
( *ptr->vptr[2] )(ptr);
( *ptr->vptr[2] )(&ref);
其中的 2 表示 VirtualFunc 在類虛函數表的第 2 個槽位。可以看出,虛函數的調
用相當于一個 C 的函數指針調用,其效率也并未降低。
由以上的四個例子可以看出,C++ 的函數調用效率依然很高。但 C++ 還是有其特殊
性,為了保證面向對象語義的正确性,C++ 編譯器會在程式員所編寫的程式基礎上,做大
量的擴充,如果程式員不了解編譯器背後所做的這些工作,就可能寫出效率不高的程式。
對于一些繼承層次很深的派生類或在成員變量中包含了很多其它類對象(如 XX 中的
m_strName 變量)的類來說,對象的建立和銷毀的開銷是相當大的,比如 XX 類的預設構
造函數,即使程式員沒有定義任何語句,編譯器依然會給其構造函數擴充以下代碼來保證
對象語義的正确性:
XX::XX()
{
// 編譯器擴充代碼所要做的工作
1、 調用父類 X 的預設構造函數
2、 設定 vptr 指向 XX 類虛函數表
3、 調用 String 類的預設構造函數構造 m_strName
};
是以為了提高效率,減少不必要的臨時對象的産生、拖延暫時不必要的對象定義、用
初始化代替指派、使用構造函數初始化清單代替在構造函數中指派等方法都能有效提高程
序的運作效率。以下舉例說明:
1、 減少臨時對象的生成。如以傳送對象引用的方式代替傳值方式來定義函數的參
數,如下例所示,傳值方式将導緻一個 XX 臨時對象的産生
效率不高的做法 高效率做法
void Function( XX xx ) void Function( const XX& xx )
{ {
//函數體 // 函數體
} }
2、 拖延暫時不必要的對象定義。在 C 中要将所有的局部變量定義在函數體頭部,
考慮到 C++ 中對象建立的開銷,這不是一個好習慣。如下例,如果大部分情況下
bCache 為 "真 ",則拖延 xx 的定義可以大大提高函數的效率。
效率不高的做法 高效率做法
void Function( bool bCache ) void Function( bool bCache )
{ {
// 函數體 // 函數體
XX xx; if (bCache)
if (bCache) {// do something without xx
{ return;
// do something without xx }
return;
}
// 對xx進行操作 XX xx;
// 對 xx 進行操作
…
return; return;
} }
3、 可能情況下,以初始化代替先定義後指派。如下例,高效率的做法會比效率不高
的做法省去了 cache 變量的預設構造函數調用開銷。
效率不高的做法 高效率做法
void Function(const XX &xx) void Function(const XX &xx)
{ {
XX cache; XX cache = xx;
cache = xx ;
} }
4、 在構造函數中使用成員變量的初始化清單代替在構造函數中指派。如下例,在效
率不高的做法中,XX 的構造函數會首先調用 m_strName 的預設構造函數,再産生一個臨
時的 String object,用空串""初始化臨時對象,再以臨時對象指派(assign)給
m_strName ,然後銷毀臨時對象。而高效的做法隻需要調用一次 m_strName 的構造函數。
效率不高的做法 高效率做法
XX::XX() XX::XX() : m_strName( "" )
{ {
m_strName = ""; …
…
} }
類似的例子還很多,如何寫出高效的 C++ 程式需要實踐和積累,但了解 C++ 的底層
運作機制是一個不可缺少的步驟,隻要平時多學習和思考,編寫高效的 C++ 程式是完全
可行的。