我正在尝试对函数指针调用和虚函数调用之间的差异进行基准测试。为此,我编写了两段代码,对数组进行相同的数学计算。一个变体使用指向函数的指针数组并在循环中调用它们。另一个变体使用指向基类的指针数组并调用其虚函数,该函数在派生类中重载,与第一个变体中的函数完全相同。然后我打印经过的时间并使用一个简单的shell脚本多次运行基准测试并计算平均运行时间。
以下是代码:
#include <iostream>
#include <cstdlib>
#include <ctime>
#include <cmath>
using namespace std;
long long timespecDiff(struct timespec *timeA_p, struct timespec *timeB_p)
{
return ((timeA_p->tv_sec * 1000000000) + timeA_p->tv_nsec) -
((timeB_p->tv_sec * 1000000000) + timeB_p->tv_nsec);
}
void function_not( double *d ) {
*d = sin(*d);
}
void function_and( double *d ) {
*d = cos(*d);
}
void function_or( double *d ) {
*d = tan(*d);
}
void function_xor( double *d ) {
*d = sqrt(*d);
}
void ( * const function_table[4] )( double* ) = { &function_not, &function_and, &function_or, &function_xor };
int main(void)
{
srand(time(0));
void ( * index_array[100000] )( double * );
double array[100000];
for ( long int i = 0; i < 100000; ++i ) {
index_array[i] = function_table[ rand() % 4 ];
array[i] = ( double )( rand() / 1000 );
}
struct timespec start, end;
clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &start);
for ( long int i = 0; i < 100000; ++i ) {
index_array[i]( &array[i] );
}
clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &end);
unsigned long long time_elapsed = timespecDiff(&end, &start);
cout << time_elapsed / 1000000000.0 << endl;
}
这是虚函数变体:
#include <iostream>
#include <cstdlib>
#include <ctime>
#include <cmath>
using namespace std;
long long timespecDiff(struct timespec *timeA_p, struct timespec *timeB_p)
{
return ((timeA_p->tv_sec * 1000000000) + timeA_p->tv_nsec) -
((timeB_p->tv_sec * 1000000000) + timeB_p->tv_nsec);
}
class A {
public:
virtual void calculate( double *i ) = 0;
};
class A1 : public A {
public:
void calculate( double *i ) {
*i = sin(*i);
}
};
class A2 : public A {
public:
void calculate( double *i ) {
*i = cos(*i);
}
};
class A3 : public A {
public:
void calculate( double *i ) {
*i = tan(*i);
}
};
class A4 : public A {
public:
void calculate( double *i ) {
*i = sqrt(*i);
}
};
int main(void)
{
srand(time(0));
A *base[100000];
double array[100000];
for ( long int i = 0; i < 100000; ++i ) {
array[i] = ( double )( rand() / 1000 );
switch ( rand() % 4 ) {
case 0:
base[i] = new A1();
break;
case 1:
base[i] = new A2();
break;
case 2:
base[i] = new A3();
break;
case 3:
base[i] = new A4();
break;
}
}
struct timespec start, end;
clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &start);
for ( int i = 0; i < 100000; ++i ) {
base[i]->calculate( &array[i] );
}
clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &end);
unsigned long long time_elapsed = timespecDiff(&end, &start);
cout << time_elapsed / 1000000000.0 << endl;
}
我的系统是LInux,Fedora 13,gcc 4.4.2。代码用g ++ -O3编译。第一个是test1,第二个是test2。
现在我在控制台中看到了这个:
[Ignat@localhost circuit_testing]$ ./test2 && ./test2
0.0153142
0.0153166
嗯,或多或少,我想。然后,这个:
[Ignat@localhost circuit_testing]$ ./test2 && ./test2
0.01531
0.0152476
应该可见的25%在哪里?第一个可执行文件如何比第二个可执行文件慢?
我问这个是因为我正在做一个项目,涉及为了计算数组的值而连续调用这样的大量小函数,而我继承的代码执行非常复杂的操作避免虚函数调用开销。现在这个着名的呼叫在哪里开销?
答案 0 :(得分:8)
在这两种情况下,您都是间接调用函数。在一种情况下通过你的函数指针表,在另一种情况下通过编译器的函数指针数组(vtable)。毫不奇怪,两个类似的操作可以为您提供类似的计时结果。
答案 1 :(得分:4)
虚函数可能比常规函数慢,但这是由内联之类的东西引起的。如果通过函数表调用函数,那么也不能内联函数,并且查找时间几乎相同。查找自己的查找表当然与查找编译器的查找表相同 编辑:甚至更慢,因为编译器比处理器缓存等知识要多得多。
答案 2 :(得分:3)
如今,在大多数系统中,内存访问是主要的瓶颈,而不是CPU。在许多情况下,虚拟和非虚函数之间几乎没有显着差异 - 它们通常只占执行时间的很小一部分。 (对不起,我没有报告数据来支持这个,只是经验数据。)
如果您希望获得最佳性能,那么如果您研究如何并行化计算以利用多个核心/处理单元,而不是担心虚拟与非虚拟的微观细节,那么您将获得更大的收益。虚函数。
答案 3 :(得分:3)
许多人养成了做事的习惯,因为他们被认为“更快”。 这都是相对的。
如果我要从家里开100英里的车程,我必须先开车到街区。我可以在街区右侧或左侧开车。其中一个将“更快”。但这有关系吗?当然不是。
在这种情况下,您调用的函数依次调用数学函数。
如果你在IDE或GDB下暂停程序,我怀疑你会发现几乎每次你暂停它都会在那些数学库例程中(或者它应该是!),并且取消引用另一个指针来到那里(假设它没有破坏缓存)应该在噪声中丢失。
已添加:这是最受欢迎的视频:Harry Porter's relay computer。由于这件事费力地撇开增加数字并踩踏其程序计数器,我发现有必要注意所有计算机正在做的事情,只是在不同的时间和复杂程度上。在您的情况下,请考虑执行sin
,cos
,tan
或sqrt
的算法。在里面,它正在匆匆做这些事情,只是偶然地跟着地址或者弄得很慢的记忆才能到达那里。
答案 4 :(得分:3)
我认为你看到了差异,但这只是函数调用开销。在两种情况下,分支错误预测,内存访问和触发功能都是相同的。与那些相比,它只是没有那么大的交易,尽管当我尝试它时,函数指针的情况肯定会快一些。
如果这是你的大型项目的代表,这是一个很好的证明,这种微观优化有时只是海洋中的一滴,最坏的是徒劳。但是把它放在一边,为了更清晰的测试,函数应该执行一些更简单的操作,这对于每个函数都是不同的:
void function_not( double *d ) {
*d = 1.0;
}
void function_and( double *d ) {
*d = 2.0;
}
依此类推,类似于虚拟功能。
(每个函数都应该做一些不同的事情,这样它们就不会被删除,并且所有函数都会以相同的地址结束;这会使分支预测工作变得不切实际。)
通过这些更改,结果会有所不同。每种情况下最好的4次运行。 (不是很科学,但是对于大量的运行,数字大致相似。)所有时间都是循环的,在我的笔记本电脑上运行。代码是用VC ++编译的(只改变了时序),但gcc以相同的方式实现了虚函数调用,因此即使使用不同的OS / x86 CPU /编译器,相对时序也应大致相似。
函数指针:2,052,770
虚拟:3,598,039
这种差异似乎有点过分了!果然,两位代码在内存访问行为方面并不完全相同。第二个应该有一个4个A * s的表,用于填充基数,而不是为每个条目新建一个新的。然后,当获取指针跳过时,两个示例将具有类似的行为(1个高速缓存未命中/ N个条目)。例如:
A *tbl[4] = { new A1, new A2, new A3, new A4 };
for ( long int i = 0; i < 100000; ++i ) {
array[i] = ( double )( rand() / 1000 );
base[i] = tbl[ rand() % 4 ];
}
有了这个,仍然使用简化的功能:
虚拟(如此处所示):2,487,699
所以有20%,最好的情况。足够接近?
所以也许你的同事至少应该考虑这一点,但我怀疑在任何现实的程序中,呼叫开销都不足以成为值得跳过篮球的瓶颈。
答案 5 :(得分:0)
最后,函数指针的方法已经证明是最快的。这是我从一开始就期待的。