C++ 性能剖析 (四):Inheritance 對性能的影響
Inheritance 是OOP 的一個重要特征。雖然業界有許多同行不喜歡inheritance,但是正確地使用inheritance是一個應用層面和架構層面的重要設計決定。 大量使用inheritance,尤其在類似std container 中使用,會對程序性能產生何等影響呢?
從我個人的經驗來看,constructor對創建具有深層inheritance鏈的class,有很大的影響。 如果應用容許,最好使用沒有constructor的基類。下面舉個例子:
struct __declspec(novtable) ITest1
{ virtual void AddRef() = 0;
virtual void Release() = 0;
virtual void DoIt(int x) = 0; };
class CTest: public ITest1
{
int ref;
public: inline CTest() { ref = 0; }
inline void AddRef() { ++ref; }
inline void Release() {--ref; }
inline void DoIt(int x) {ref *= x; }
inline void AddRef2() { ++ref; }
inline void Release2() {--ref; }
inline void DoIt2(int x) {ref *= x; }
static void TestPerf(int loop); };
這是個dummy程序,然而在COM中確是再常見不過。如果我們要大量創建並使用CTest,有經驗的程序員應該看出,ITest1 完全不需要constructor。 根據C++ 說明書,ITest1因為有虛擬函數,屬於“非簡單構造類”,編譯必須產生一個constructor,其唯一的目的是設置ITest1的vtbl (虛擬函數表)。
然而interface的唯一作用是被繼承,所以其vtbl一定是被其繼承類設置。編譯在這種情況下沒必要生成constructor。 微軟在設計ATL時認識到這一點,推出自己的方案來躲避C++官方SPEC的缺陷:VC++提供了novtable的class modifier,告訴編譯:我不需要你的constructor. 然而我在VS 2010中的測試結果卻令人失望:
ITest1的constructor 仍然被生成了,只是它沒有將vtbl賦值而已,這對增進基類構造的性能實為杯水車薪之舉。 下面我們看看這個“毫無用處的constructor”對性能的影響。 我們權且拿出另一個不需要虛擬函數的ITestPOD (POD的意思是“數據而已”)來做比較:
struct ITest1POD
{ inline void AddRef() { }
inline void Release() { }
inline void DoIt(int x) { } };
ITestPOD當然不能完全作interface用(interface必須用虛擬函數),僅僅為了測試。然後,我們設計一個繼承類,和上面的CTest功能完全一樣:
class CTestPOD: public ITest1POD
{
int ref;
public: inline CTestPOD() { ref = 0; }
inline void AddRef() { ++ref; }
inline void Release() {--ref; }
inline void DoIt(int x) {ref *= x; }
};
我們的目的是用這個CTestPOD來和CTest作一番蘋果與蘋果的比較:
void CTest::TestPerf(int loop)
{
clock_t begin = clock();
for(int i = 0; i < loop; ++i) //loop1
{
CTestPOD testPOD; // line1
testPOD.AddRef();
testPOD.DoIt(0);
testPOD.Release();
}
clock_t end = clock();
printf("POD time: %f \n",double(end - begin) / CLOCKS_PER_SEC);
begin = clock();
for(int i = 0; i < loop; ++i) //loop2
{
CTest test; // line2
test.AddRef2();
test.DoIt2(0);
test.Release2();
}
end = clock();
printf("Interface time: %f \n",double(end - begin) / CLOCKS_PER_SEC);
}
上面的loop1和loop2的唯一區別在line1和line2,為了避免用虛擬函數,我特意給CTest准備了AddRef2,DoIt2,Release2,三個同樣的但卻是非虛擬的函數,為的是遵循性能測試的一大原理:compare apple to apple。
我將loop設為10萬,測試結果顯示,loop2比loop1的速度低了20% 左右。從生成的代碼來看,唯一的區別是CTest的constructor調用了編譯自動生成的ITest1 的constructor。這個constructor沒有任何作用,卻白占了許多CPU周期。一個好的編譯,應該是可以把這個constructor裁剪掉的,這個靠我們自己去搜索了。
總結
在應用inheritance時,除去基類裡無用的constructor,對大量構造的object的性能來說,會有明顯的影響。不幸的是,微軟的__declspec(novtable) class modifier對解決這個問題沒有提供任何幫助。在設計海量存儲的object的應用中,我們應該盡量用POD來做其基類,避免上面CTest類那樣明顯的性能漏洞。