虚函数调用开销在哪里?

时间:2010-05-16 14:27:56

标签: c++ optimization virtual

我正在尝试对函数指针调用和虚函数调用之间的差异进行基准测试。为此,我编写了两段代码,对数组进行相同的数学计算。一个变体使用指向函数的指针数组并在循环中调用它们。另一个变体使用指向基类的指针数组并调用其虚函数,该函数在派生类中重载,与第一个变体中的函数完全相同。然后我打印经过的时间并使用一个简单的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%在哪里?第一个可执行文件如何比第二个可执行文件慢?

我问这个是因为我正在做一个项目,涉及为了计算数组的值而连续调用这样的大量小函数,而我继承的代码执行非常复杂的操作避免虚函数调用开销。现在这个着名的呼叫在哪里开销?

6 个答案:

答案 0 :(得分:8)

在这两种情况下,您都是间接调用函数。在一种情况下通过你的函数指针表,在另一种情况下通过编译器的函数指针数组(vtable)。毫不奇怪,两个类似的操作可以为您提供类似的计时结果。

答案 1 :(得分:4)

虚函数可能比常规函数慢,但这是由内联之类的东西引起的。如果通过函数表调用函数,那么也不能内联函数,并且查找时间几乎相同。查找自己的查找表当然与查找编译器的查找表相同 编辑:甚至更慢,因为编译器比处理器缓存等知识要多得多。

答案 2 :(得分:3)

如今,在大多数系统中,内存访问是主要的瓶颈,而不是CPU。在许多情况下,虚拟和非虚函数之间几乎没有显着差异 - 它们通常只占执行时间的很小一部分。 (对不起,我没有报告数据来支持这个,只是经验数据。)

如果您希望获得最佳性能,那么如果您研究如何并行化计算以利用多个核心/处理单元,而不是担心虚拟与非虚拟的微观细节,那么您将获得更大的收益。虚函数。

答案 3 :(得分:3)

许多人养成了做事的习惯,因为他们被认为“更快”。 这都是相对的。

如果我要从家里开100英里的车程,我必须先开车到街区。我可以在街区右侧或左侧开车。其中一个将“更快”。但这有关系吗?当然不是。

在这种情况下,您调用的函数依次调用数学函数。

如果你在IDE或GDB下暂停程序,我怀疑你会发现几乎每次你暂停它都会在那些数学库例程中(或者它应该是!),并且取消引用另一个指针来到那里(假设它没有破坏缓存)应该在噪声中丢失。

已添加:这是最受欢迎的视频:Harry Porter's relay computer。由于这件事费力地撇开增加数字并踩踏其程序计数器,我发现有必要注意所有计算机正在做的事情,只是在不同的时间和复杂程度上。在您的情况下,请考虑执行sincostansqrt的算法。在里面,它正在匆匆做这些事情,只是偶然地跟着地址或者弄得很慢的记忆才能到达那里。

答案 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)

最后,函数指针的方法已经证明是最快的。这是我从一开始就期待的。