天天看點

C++中虛函數(virtual function)到底有多慢

本文位址: http://blog.csdn.net/hengyunabc/article/details/7461919

虛函數為什麼慢,cpu分支預測技術,虛函數到底要調用哪些彙編,虛函數實作的簡單圖示,虛函數不能内聯,

印象中經常看到有人批評C++的虛函數很慢,但是虛函數為什麼慢,虛函數到底有多慢呢?

一、理論分析

虛函數慢的原因主要有三個:

  1. 多了幾條彙編指令(運作時得到對應類的函數的位址)
  2. 影響cpu流水線
  3. 編譯器不能内聯優化(僅在用父類引用或者指針調用時,不能内聯)

先簡單說下虛函數的實作,以下面測試代碼中的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億次調用所花的時間是幾秒,十幾秒,且你不能容忍它慢一二秒,那麼就幹掉虛函數吧^_^。

三、總結:

  1. 虛函數調用效率和繼承層數無關;
  2. 其實虛函數還是挺快的。
  3. 如果真的要完全移除虛函數,那麼如果要實作運作時多态,則要用到函數指針,據上面的分析,函數指針基本具有虛函數的所有缼點(要傳遞函數指針,同樣無法内聯,同樣影響流水線),且函數指針會使代碼混亂。

BTW:測試cpu是i5。

TODO:測試指針函數,boost::bind的效率

繼續閱讀