代码之家  ›  专栏  ›  技术社区  ›  iksemyonov

虚拟函数调用开销在哪里?

  •  3
  • iksemyonov  · 技术社区  · 15 年前

    我正在尝试对函数指针调用和虚拟函数调用之间的差异进行基准测试。为此,我编写了两段代码,它们对一个数组进行相同的数学计算。一个变量使用指向函数的指针数组并调用循环中的函数。另一个变量使用指向基类的指针数组并调用其虚函数,派生类中的虚函数被重载,以执行与第一个变量中的函数完全相同的操作。然后我打印所用的时间,并使用一个简单的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、Fedora13、GCC4.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 回复  |  直到 15 年前
        1
  •  3
  •   please delete me    15 年前

    我认为您看到了不同之处,但这只是函数调用开销。在这两种情况下,分支预测失误、内存访问和trig函数都是相同的。与这些相比,这没什么大不了的,尽管函数指针的情况在我尝试的时候确实快了一点。

    如果这是您的大型程序的代表,这是一个很好的证明,这种类型的微优化有时只是沧海一粟,最糟糕的是徒劳无功。但撇开这一点不谈,为了进行更清晰的测试,函数应该执行一些更简单的操作,这对于每个函数都是不同的:

    void function_not( double *d ) {
        *d = 1.0;
    }
    
    void function_and( double *d ) {
        *d = 2.0;
    }
    

    等等,类似于虚拟函数。

    (每个函数都应该做一些不同的事情,这样它们就不会被省略,最终得到相同的地址;这将使分支预测工作不切实际。)

    有了这些变化,结果有点不同。每种情况下最好4次。(不是很科学,但是对于较大的运行次数来说,这些数字大体上是相似的。)所有的时间都是循环的,在我的笔记本电脑上运行。代码是用vc++编译的(只改变了时间),但gcc以同样的方式实现虚拟函数调用,因此即使使用不同的OS/x86 CPU/编译器,相对时间也应该大致相似。

    函数指针:2052770

    虚拟:3598039

    这种差别似乎有点过分!当然,这两位代码在内存访问行为方面并不完全相同。第二张桌子应该有一张4a*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 ];
    }
    

    在这种情况下,仍然使用简化的函数:

    虚拟(此处建议):2487699

    所以有20%是最好的。足够接近吗?

    所以,也许你的同事至少考虑到这一点是正确的,但我怀疑在任何现实的程序中,呼叫开销都不足以成为一个瓶颈,不值得跳过去。

        2
  •  8
  •   anon    15 年前

    在这两种情况下,都是间接调用函数。一种是通过函数指针表,另一种是通过编译器的函数指针数组(vtable)。毫不奇怪,两个相似的操作会给您带来相似的计时结果。

        3
  •  4
  •   Puppy    15 年前

    虚拟函数可能比常规函数慢,但这是由于诸如inline之类的原因造成的。如果您通过函数表调用一个函数,那么这些函数也不能被内联,并且查找时间几乎相同。通过自己的查找表进行查找当然与通过编译器的查找表进行查找相同。
    编辑:或者更慢,因为编译器比你更了解处理器缓存之类的东西。

        4
  •  3
  •   mdma    15 年前

    现在,在大多数系统中,内存访问是主要的瓶颈,而不是CPU。在许多情况下,虚函数和非虚函数之间几乎没有显著的区别——它们通常只代表执行时间的一小部分。(不好意思,我没有报告数据来支持这一点,只是实证数据。)

    如果你想获得最好的性能,如果你研究如何并行计算以利用多个核心/处理单元,而不是担心虚函数和非虚函数的微观细节,你将得到更多的回报。

        5
  •  3
  •   Mike Dunlavey    15 年前

    许多人之所以养成这样的习惯,仅仅是因为他们被认为“更快”。 都是相对的。

    如果我要从家里开100英里的车,我必须先绕着街区开车。我可以绕街区向右或向左行驶。其中之一就是“更快”。但这有关系吗?当然不是。

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

    如果您在IDE或GDB下暂停程序,我怀疑您会发现,几乎每次暂停程序时,它都会出现在那些数学库例程中(或者应该出现!),并且取消引用一个额外的指向那里的指针(假设它不会破坏缓存)应该在噪声中丢失。

    添加:以下是最喜爱的视频: Harry Porter's relay computer . 当这件事费劲地敲打着加上数字和步进它的程序计数器时,我发现记住这是所有计算机都在做的,只是在不同的时间和复杂程度上。在你的例子中,考虑一个算法 sin , cos , tan sqrt . 在里面,它在做那些事情,而且只是偶然地跟随地址或与一个真正缓慢的记忆混乱到那里。

        6
  •  0
  •   iksemyonov    15 年前

    最后,函数指针方法被证明是最快的方法。这是我从一开始就预料到的。