本文位址: http://blog.csdn.net/hengyunabc/article/details/7461919
虛函數為什麼慢,cpu分支預測技術,虛函數到底要調用哪些彙編,虛函數實作的簡單圖示,虛函數不能内聯,
印象中經常看到有人批評C++的虛函數很慢,但是虛函數為什麼慢,虛函數到底有多慢呢?
一、理論分析
虛函數慢的原因主要有三個:
- 多了幾條彙編指令(運作時得到對應類的函數的位址)
- 影響cpu流水線
- 編譯器不能内聯優化(僅在用父類引用或者指針調用時,不能内聯)
先簡單說下虛函數的實作,以下面測試代碼中的VirtualVector類為例,VirtualVector類的記憶體布局如下:
當在用父類的引用或者指針調用虛函數時,會先從該對象的頭部取到虛函數的位址(C++标準規定虛函數表位址必須放最前),再從虛函數表中取到實際要調用的函數的位址,最終調用該函數,彙編代碼大概如下:
sum += v.at(i); //要調用at函數
00CF1305 mov eax,dword ptr [ebx] //取到對象的虛函數表位址
00CF1307 mov edx,dword ptr [eax+4] //取到實際VirtualVector類的at函數位址,因為at是第二個虛函數,是以要+4,如果是clear則+8,push_back則不加
00CF130A push esi //參數壓棧
00CF130B mov ecx,ebx
00CF130D call edx //調用真正的VirtualVector類的at函數
是以,我們可以看到調用虛函數,相比普通函數,實際上多了三條彙編指令(取虛表,取函數位址,call調用)。
至于虛函數如何影響cpu的流水線,則主要是因為call指令,具體可以看這個網址的示範:
CPU流水線的一個示範:
http://www.pictutorials.com/Instruction_Pipeline.html第3點,編譯器不能内聯優化,則是因為在得到子類的引用或者指針之前,根本不知道要調用哪一個函數,是以無從内聯。
但是,要注意的是,對于子類直接調用虛函數,是可以内聯優化的。如以下的代碼,編譯器是完全可以内聯展開的。
VirtualVector v(100);
v.push_back(1);
v.at(0);
二、實際測試
光說不練顯然不行,下面用代碼來測試下虛函數到底有多慢。
下面的代碼隻是測試用,不考慮細節。
Vector類包裝了一個數組,提供push_back,at,clear函數。
VirtualVector類繼承了IVector,同樣實作了push_back,at,clear函數,但是都是虛函數。
#include <iostream>
#include <time.h>
#include <vector>
using namespace std;
const int size = 100000000;
class Vector{
private:
int *array;
int pos;
public:
Vector(int size):array(new int[size]),pos(0)
{
}
void push_back(int val)
{
array[pos++] = val;
}
int at(int i)
{
return array[i];
}
void clear()
{
pos = 0;
}
};
class IVector{
public:
virtual void push_back(int val) = 0;
virtual int at(int i) = 0;
virtual void clear() = 0;
virtual ~IVector() {};
};
class VirtualVector : public IVector{
public:
int *array;
int pos;
public:
VirtualVector(int size):array(new int[size]),pos(0)
{
}
void push_back(int val)
{
array[pos++] = val;
}
int at(int i)
{
return array[i];
}
void clear()
{
pos = 0;
}
~VirtualVector()
{
if(array != NULL)
delete array;
}
};
void testVectorPush(Vector& v){
v.clear();
clock_t nTimeStart; //計時開始
clock_t nTimeStop; //計時結束
nTimeStart = clock(); //
for(int i = 0; i < size; ++i)
{
v.push_back(i);
//cout<<v.size()<<endl;
}
nTimeStop = clock(); //
cout <<"耗時:"<<(double)(nTimeStop - nTimeStart)/CLOCKS_PER_SEC<<"秒"<< endl;
}
void testVectorAt(Vector& v)
{
clock_t nTimeStart; //計時開始
clock_t nTimeStop; //計時結束
int sum = 0;
nTimeStart = clock(); //
for(int j = 0; j < 1; ++j)
{
for(int i = 0; i < size; ++i)
{
sum += v.at(i);
}
}
nTimeStop = clock(); //
cout <<"耗時:"<<(double)(nTimeStop - nTimeStart)/CLOCKS_PER_SEC<<"秒"<< endl;
cout<<"sum:"<<sum<<endl;
}
void testVirtualVectorPush(IVector& v)
{
v.clear();
clock_t nTimeStart; //計時開始
clock_t nTimeStop; //計時結束
nTimeStart = clock(); //
for(int i = 0; i < size; ++i)
{
v.push_back(i);
//cout<<v.size()<<endl;
}
nTimeStop = clock(); //
cout <<"耗時:"<<(double)(nTimeStop - nTimeStart)/CLOCKS_PER_SEC<<"秒"<< endl;
}
void testVirtualVectorAt(IVector& v)
{
clock_t nTimeStart; //計時開始
clock_t nTimeStop; //計時結束
int sum = 0;
nTimeStart = clock(); //
for(int j = 0; j < 1; ++j)
{
for(int i = 0; i < size; ++i)
{
sum += v.at(i);
}
}
nTimeStop = clock(); //
cout <<"耗時:"<<(double)(nTimeStop - nTimeStart)/CLOCKS_PER_SEC<<"秒"<< endl;
cout<<"sum:"<<sum<<endl;
}
int main()
{
cout<<sizeof(VirtualVector)<<endl;
Vector *v = new Vector(size);
VirtualVector *V = new VirtualVector(size);
cout<<"testVectorPush:"<<endl;
testVectorPush(*v);
testVectorPush(*v);
testVectorPush(*v);
testVectorPush(*v);
cout<<"testVirtualVectorPush:"<<endl;
testVirtualVectorPush(*V);
testVirtualVectorPush(*V);
testVirtualVectorPush(*V);
testVirtualVectorPush(*V);
cout<<"testVectorAt:"<<endl;
testVectorAt(*v);
testVectorAt(*v);
testVectorAt(*v);
testVectorAt(*v);
cout<<"testVirtualVectorAt:"<<endl;
testVirtualVectorAt(*V);
testVirtualVectorAt(*V);
testVirtualVectorAt(*V);
testVirtualVectorAt(*V);
return 0;
}
上面的是隻有一層繼承的情況時的結果,盡管從虛函數的實作角度來看,多層繼承和一層繼承調用虛函數的效率都是一樣的。
但是為了測試結果更加可信,下面是一個6層繼承的測試代碼(為了防止編譯器的優化,有很多垃圾代碼):
#include <iostream>
#include <time.h>
#include <vector>
using namespace std;
const int size = 100000000;
class Vector{
private:
int *array;
int pos;
public:
Vector(int size):array(new int[size]),pos(0)
{
}
void push_back(int val)
{
array[pos++] = val;
}
int at(int i)
{
return array[i];
}
void clear()
{
pos = 0;
}
};
class IVector{
public:
virtual void push_back(int val) = 0;
virtual int at(int i) = 0;
virtual void clear() = 0;
virtual ~IVector() {};
};
class VirtualVector1 : public IVector{
public:
int *array;
int pos;
public:
VirtualVector1(int size):array(new int[size]),pos(0)
{
}
void push_back(int val)
{
array[1] = val;
}
int at(int i)
{
return array[1];
}
void clear()
{
pos = 0;
}
~VirtualVector1()
{
if(array != NULL)
delete array;
}
};
class VirtualVector2 : public VirtualVector1{
public:
VirtualVector2(int size):VirtualVector1(size)
{
}
void push_back(int val)
{
array[2] = val;
}
int at(int i)
{
return array[2];
}
void clear()
{
pos = 0;
}
};
class VirtualVector3 : public VirtualVector2{
public:
VirtualVector3(int size):VirtualVector2(size)
{
}
void push_back(int val)
{
array[3] = val;
}
int at(int i)
{
return array[3];
}
void clear()
{
pos = 0;
}
};
class VirtualVector4 : public VirtualVector3{
public:
VirtualVector4(int size):VirtualVector3(size)
{
}
void push_back(int val)
{
array[4] = val;
}
int at(int i)
{
return array[4];
}
void clear()
{
pos = 0;
}
};
class VirtualVector5 : public VirtualVector4{
public:
VirtualVector5(int size):VirtualVector4(size)
{
}
void push_back(int val)
{
array[5] = val;
}
int at(int i)
{
return array[5];
}
void clear()
{
pos = 0;
}
};
class VirtualVector6 : public VirtualVector5{
public:
VirtualVector6(int size):VirtualVector5(size)
{
}
void push_back(int val)
{
array[6] = val;
}
int at(int i)
{
return array[6];
}
void clear()
{
pos = 0;
}
};
class VirtualVector : public VirtualVector6{
public:
VirtualVector(int size):VirtualVector6(size)
{
}
void push_back(int val)
{
array[pos++] = val;
}
int at(int i)
{
return array[i];
}
void clear()
{
pos = 0;
}
};
void testVectorPush(Vector& v){
v.clear();
clock_t nTimeStart; //計時開始
clock_t nTimeStop; //計時結束
nTimeStart = clock(); //
for(int i = 0; i < size; ++i)
{
v.push_back(i);
//cout<<v.size()<<endl;
}
nTimeStop = clock(); //
cout <<"耗時:"<<(double)(nTimeStop - nTimeStart)/CLOCKS_PER_SEC<<"秒"<< endl;
}
void testVectorAt(Vector& v)
{
clock_t nTimeStart; //計時開始
clock_t nTimeStop; //計時結束
int sum = 0;
nTimeStart = clock(); //
for(int j = 0; j < 1; ++j)
{
for(int i = 0; i < size; ++i)
{
sum += v.at(i);
}
}
nTimeStop = clock(); //
cout <<"耗時:"<<(double)(nTimeStop - nTimeStart)/CLOCKS_PER_SEC<<"秒"<< endl;
cout<<"sum:"<<sum<<endl;
}
void testVirtualVectorPush(IVector& v)
{
v.clear();
clock_t nTimeStart; //計時開始
clock_t nTimeStop; //計時結束
nTimeStart = clock(); //
for(int i = 0; i < size; ++i)
{
v.push_back(i);
//cout<<v.size()<<endl;
}
nTimeStop = clock(); //
cout <<"耗時:"<<(double)(nTimeStop - nTimeStart)/CLOCKS_PER_SEC<<"秒"<< endl;
}
void testVirtualVectorAt(IVector& v)
{
clock_t nTimeStart; //計時開始
clock_t nTimeStop; //計時結束
int sum = 0;
nTimeStart = clock(); //
for(int j = 0; j < 1; ++j)
{
for(int i = 0; i < size; ++i)
{
sum += v.at(i);
}
}
nTimeStop = clock(); //
cout <<"耗時:"<<(double)(nTimeStop - nTimeStart)/CLOCKS_PER_SEC<<"秒"<< endl;
cout<<"sum:"<<sum<<endl;
}
int main()
{
cout<<sizeof(VirtualVector)<<endl;
{
auto v = VirtualVector1(size);
v.push_back(0);
cout<<v.at(0)<<endl;
}
{
auto v = VirtualVector2(size);
v.push_back(0);
cout<<v.at(0)<<endl;
}
{
auto v = VirtualVector3(size);
v.push_back(0);
cout<<v.at(0)<<endl;
}
{
auto v = VirtualVector4(size);
v.push_back(0);
cout<<v.at(0)<<endl;
}
{
auto v = VirtualVector5(size);
v.push_back(0);
cout<<v.at(0)<<endl;
}
{
auto v = VirtualVector6(size);
v.push_back(0);
cout<<v.at(0)<<endl;
}
auto *v = new Vector(size);
auto *V = new VirtualVector(size);
cout<<"testVectorPush:"<<endl;
testVectorPush(*v);
testVectorPush(*v);
testVectorPush(*v);
testVectorPush(*v);
cout<<"testVirtualVectorPush:"<<endl;
testVirtualVectorPush(*V);
testVirtualVectorPush(*V);
testVirtualVectorPush(*V);
testVirtualVectorPush(*V);
cout<<"testVectorAt:"<<endl;
testVectorAt(*v);
testVectorAt(*v);
testVectorAt(*v);
testVectorAt(*v);
cout<<"testVirtualVectorAt:"<<endl;
testVirtualVectorAt(*V);
testVirtualVectorAt(*V);
testVirtualVectorAt(*V);
testVirtualVectorAt(*V);
return 0;
}
測試結果:
測試結果都取最小時間
1層繼承的測試結果:
push_back | at | |
Vector | 0.263s | 0.04s |
VirtualVector | 0.331s | 0.222s |
倍數 | 1.25 | 5.55 |
6層繼承的測試結果:
0.262s | 0.041s | |
0.334s | 0.223s | |
1.27 | 5.43 |
一、可以看出繼承層數和虛函數調用效率無關
二、可以看出虛函數慢得有點令人發指了,對于at操作,虛函數花的時間竟然是普通函數的5.5倍!
但是,再看看,我們可以發現對于push_back操作,虛函數花的時間是普通函數的1.25倍。why?
再分析下代碼,我們可以發現at操作的邏輯,明顯要比push_back的邏輯要簡單。
虛函數額外消耗時間為 vt,函數本身所消耗時間為 ft,則有以下
倍數 = (vt + ft)/ft = 1 + vt/ft
顯然當ft越大,即函數本身消耗時間越長,則倍數越小。
那麼讓我們在at操作中加了額外代碼,統計下1到100之和:
int at(int i)
{
sssForTest = 0;
for(int j = 0; j < 100; ++j)
sssForTest += j;
return array[i];
}
測試代碼:
#include <iostream>
#include <time.h>
#include <vector>
using namespace std;
const int size = 100000000;
int sssForTest = 0;
class Vector{
private:
int *array;
int pos;
public:
Vector(int size):array(new int[size]),pos(0)
{
}
void push_back(int val)
{
array[pos++] = val;
}
int at(int i)
{
sssForTest = 0;
for(int j = 0; j < 100; ++j)
sssForTest += j;
return array[i];
}
void clear()
{
pos = 0;
}
};
class IVector{
public:
virtual void push_back(int val) = 0;
virtual int at(int i) = 0;
virtual void clear() = 0;
virtual ~IVector() {};
};
class VirtualVector : public IVector{
public:
int *array;
int pos;
public:
VirtualVector(int size):array(new int[size]),pos(0)
{
}
void push_back(int val)
{
array[pos++] = val;
}
int at(int i)
{
sssForTest = 0;
for(int j = 0; j < 100; ++j)
sssForTest += j;
return array[i];
}
void clear()
{
pos = 0;
}
~VirtualVector()
{
if(array != NULL)
delete array;
}
};
void testVectorPush(Vector& v){
v.clear();
clock_t nTimeStart; //計時開始
clock_t nTimeStop; //計時結束
nTimeStart = clock(); //
for(int i = 0; i < size; ++i)
{
v.push_back(i);
//cout<<v.size()<<endl;
}
nTimeStop = clock(); //
cout <<"耗時:"<<(double)(nTimeStop - nTimeStart)/CLOCKS_PER_SEC<<"秒"<< endl;
}
void testVectorAt(Vector& v)
{
clock_t nTimeStart; //計時開始
clock_t nTimeStop; //計時結束
int sum = 0;
nTimeStart = clock(); //
for(int j = 0; j < 1; ++j)
{
for(int i = 0; i < size; ++i)
{
sum += v.at(i);
}
}
nTimeStop = clock(); //
cout <<"耗時:"<<(double)(nTimeStop - nTimeStart)/CLOCKS_PER_SEC<<"秒"<< endl;
cout<<"sum:"<<sum<<endl;
}
void testVirtualVectorPush(IVector& v)
{
v.clear();
clock_t nTimeStart; //計時開始
clock_t nTimeStop; //計時結束
nTimeStart = clock(); //
for(int i = 0; i < size; ++i)
{
v.push_back(i);
//cout<<v.size()<<endl;
}
nTimeStop = clock(); //
cout <<"耗時:"<<(double)(nTimeStop - nTimeStart)/CLOCKS_PER_SEC<<"秒"<< endl;
}
void testVirtualVectorAt(IVector& v)
{
clock_t nTimeStart; //計時開始
clock_t nTimeStop; //計時結束
int sum = 0;
nTimeStart = clock(); //
for(int j = 0; j < 1; ++j)
{
for(int i = 0; i < size; ++i)
{
sum += v.at(i);
}
}
nTimeStop = clock(); //
cout <<"耗時:"<<(double)(nTimeStop - nTimeStart)/CLOCKS_PER_SEC<<"秒"<< endl;
cout<<"sum:"<<sum<<endl;
}
int main()
{
cout<<sizeof(VirtualVector)<<endl;
Vector *v = new Vector(size);
VirtualVector *V = new VirtualVector(size);
cout<<"testVectorPush:"<<endl;
testVectorPush(*v);
testVectorPush(*v);
testVectorPush(*v);
testVectorPush(*v);
cout<<"testVirtualVectorPush:"<<endl;
testVirtualVectorPush(*V);
testVirtualVectorPush(*V);
testVirtualVectorPush(*V);
testVirtualVectorPush(*V);
cout<<"testVectorAt:"<<endl;
testVectorAt(*v);
testVectorAt(*v);
testVectorAt(*v);
testVectorAt(*v);
cout<<"testVirtualVectorAt:"<<endl;
testVirtualVectorAt(*V);
testVirtualVectorAt(*V);
testVirtualVectorAt(*V);
testVirtualVectorAt(*V);
return 0;
}
at操作中增加求和後的統計結果:
at 增加求和代碼 | ||
0.265s | 6.893s | |
0.328s | 7.125s | |
1.23 | 1.03 |
隻是簡單增加了一個求和代碼,我們可以看到,倍數變成了1.03,也就是說虛函數的消耗基本可以忽略了。
是以說,虛函數的效率到底低不低和實際要調用的函數的耗時有關,當函數本身的的耗時越長,則虛函數的影響則越小。
再從另一個角度來看,一個虛函數調用到底額外消耗了多長時間?
從統計資料來看100,000,000次函數調用,虛函數總共額外消耗了0.05~0.23秒(VirtualVector對應時間減去Vector時間),
也就是說,1億次調用,虛函數額外花的時間是0.x到2.3秒。
也就是說,如果你有個函數,要被調用1億次,而這1億次調用所花的時間是幾秒,十幾秒,且你不能容忍它慢一二秒,那麼就幹掉虛函數吧^_^。
三、總結:
- 虛函數調用效率和繼承層數無關;
- 其實虛函數還是挺快的。
- 如果真的要完全移除虛函數,那麼如果要實作運作時多态,則要用到函數指針,據上面的分析,函數指針基本具有虛函數的所有缼點(要傳遞函數指針,同樣無法内聯,同樣影響流水線),且函數指針會使代碼混亂。
BTW:測試cpu是i5。
TODO:測試指針函數,boost::bind的效率