虚函数和性能 - C ++

时间:2009-01-16 08:22:36

标签: c++ performance optimization virtual-functions

在我的课堂设计中,我广泛使用抽象类和虚函数。我感觉虚拟功能会影响性能。这是真的?但我认为这种性能差异并不明显,看起来我正在做过早的优化。正确?

15 个答案:

答案 0 :(得分:162)

你的问题让我很好奇,所以我继续在我们使用的3GHz有序PowerPC CPU上运行一些时间。我运行的测试是使用get / set函数

创建一个简单的4d向量类
class TestVec 
{
    float x,y,z,w; 
public:
    float GetX() { return x; }
    float SetX(float to) { return x=to; }  // and so on for the other three 
}

然后我设置了三个阵列,每个阵列包含1024个这些矢量(足够小以适合L1)并运行一个循环,将它们相互添加(A.x = B.x + C.x)1000次。我使用定义为inlinevirtual和常规函数调用的函数运行它。结果如下:

  • 内联:8ms(每次通话0.65ns)
  • 直接:68毫秒(每通话5.53ns)
  • 虚拟:160毫秒(每通话13秒)

因此,在这种情况下(一切都适合缓存),虚函数调用比内联调用慢约20倍。但这究竟意味着什么呢?通过循环的每次行程都会导致3 * 4 * 1024 = 12,288函数调用(1024个向量乘以四个组件乘以每次添加三次调用),因此这些时间代表1000 * 12,288 = 12,288,000个函数调用。虚拟循环比直接循环花费了92m​​s,因此每个调用的额外开销是每个函数7 纳秒

由此我得出结论:,虚拟函数比直接函数慢得多,而没有,除非你计划每秒调用它们一千万次,没关系。

另请参阅:comparison of the generated assembly.

答案 1 :(得分:86)

一个好的经验法则是:

  

在您证明这一点之前,这不是性能问题。

使用虚拟功能会对性能产生轻微影响,但不太可能影响应用程序的整体性能。寻找性能改进的更好地方是算法和I / O.

一篇关于虚函数(以及更多)的优秀文章是Member Function Pointers and the Fastest Possible C++ Delegates

答案 2 :(得分:42)

当Objective-C(其中所有方法都是虚拟的)是iPhone的主要语言并且' Java 是Android的主要语言时,我认为使用C ++虚拟函数是非常安全的。我们的3 GHz双核塔。

答案 3 :(得分:33)

在性能非常关键的应用程序(如视频游戏)中,虚拟函数调用可能太慢。使用现代硬件,最大的性能问题是缓存未命中。如果数据不在缓存中,则在可用之前可能需要数百个周期。

当CPU获取新函数的第一条指令并且它不在缓存中时,正常的函数调用可以生成指令缓存未命中。

虚函数调用首先需要从对象加载vtable指针。这可能导致数据缓存未命中。然后它从vtable加载函数指针,这可能导致另一个数据缓存未命中。然后它调用函数,这可能导致指令缓存未命中,就像非虚函数一样。

在许多情况下,两个额外的缓存未命中并不是一个问题,但在性能关键代码的紧密循环中,它可以大大降低性能。

答案 4 :(得分:27)

来自Agner Fog's "Optimizing Software in C++" manual的第44页:

  

调用虚拟成员函数所花费的时间比调用非虚拟成员函数所花费的时间长几个,前提是函数调用语句始终调用相同版本的虚函数。如果版本发生变化,那么您将得到10到30个时钟周期的错误预测惩罚。虚函数调用的预测和误预测规则与switch语句相同......

答案 5 :(得分:7)

绝对。当计算机以100Mhz运行时,这是一个问题,因为每个方法调用都需要在调用vtable之前查找它。但是今天......在3Ghz CPU上有一级缓存,内存比第一台计算机多?一点也不。从主RAM分配内存将花费您比所有功能都是虚拟的更多时间。

就像过去的旧时代,人们说结构化编程很慢,因为所有的代码都被分成了函数,每个函数都需要堆栈分配和函数调用!

我唯一想过考虑虚拟函数对性能影响的困扰,就是它是否被大量使用并在模板化代码中实例化,最终贯穿于所有内容。即便如此,我也不会花太多精力!

PS想到其他“易于使用”的语言 - 他们所有的方法都是虚拟的,他们现在不会爬行。

答案 6 :(得分:6)

除了执行时间之外还有另一个性能标准。 Vtable也占用了内存空间,在某些情况下可以避免:ATL使用编译时“simulated dynamic binding”与templates来获得“静态多态”的效果,这有点难解释;您基本上将派生类作为参数传递给基类模板,因此在编译时基类“知道”它在每个实例中的派生类。不允许您在基类型集合(即运行时多态性)中存储多个不同的派生类,但是从静态意义上说,如果您想创建一个与预先存在的模板类X相同的类Y,它具有这种覆盖的钩子,你只需要覆盖你关心的方法,然后你得到类X的基本方法,而不必有一个vtable。

在具有大内存占用的类中,单个vtable指针的成本并不多,但COM中的一些ATL类非常小,如果运行时多态性情况永远不会发生,那么值得节省vtable发生。

另见this other SO question

顺便提一下这里讨论CPU时间性能方面的a posting I found

答案 7 :(得分:4)

是的,你是对的,如果你对虚函数调用的成本感到好奇,你可能会发现this post很有意思。

答案 8 :(得分:3)

我能看到虚拟函数成为性能问题的唯一方法是在紧密循环中调用许多虚函数,而当且仅当导致页面错误或其他“重”记忆操作即将发生。

虽然像其他人一样说在现实生活中对你来说几乎不会成为问题。如果您认为是这样,运行一个分析器,进行一些测试,并在尝试“取消签名”代码以获得性能优势之前验证这是否确实存在问题。

答案 9 :(得分:3)

当类方法不是虚拟的时,编译器通常会进行内联。相反,当您使用指向具有虚函数的某个类的指针时,实际地址仅在运行时才会知道。

测试很好地说明了这一点,时差〜700%(!):

#include <time.h>

class Direct
{
public:
    int Perform(int &ia) { return ++ia; }
};

class AbstrBase
{
public:
    virtual int Perform(int &ia)=0;
};

class Derived: public AbstrBase
{
public:
    virtual int Perform(int &ia) { return ++ia; }
};


int main(int argc, char* argv[])
{
    Direct *pdir, dir;
    pdir = &dir;

    int ia=0;
    double start = clock();
    while( pdir->Perform(ia) );
    double end = clock();
    printf( "Direct %.3f, ia=%d\n", (end-start)/CLOCKS_PER_SEC, ia );

    Derived drv;
    AbstrBase *ab = &drv;

    ia=0;
    start = clock();
    while( ab->Perform(ia) );
    end = clock();
    printf( "Virtual: %.3f, ia=%d\n", (end-start)/CLOCKS_PER_SEC, ia );

    return 0;
}

虚函数调用的影响很大程度上取决于情况。 如果在功能内部几乎没有电话和大量工作 - 它可以忽略不计。

或者,当它是多次重复使用的虚拟呼叫时,在做一些简单的操作时 - 它可能非常大。

答案 10 :(得分:2)

在我的特定项目中,我至少20次来回走动。虽然可以在代码重用,清晰度,可维护性和可读性方面获得一些巨大的收益,但另一方面,性能命中仍然存在虚拟功能。

在现代笔记本电脑/台式机/平板电脑上,性能是否会受到惊吓......可能不是!但是,在嵌入式系统的某些情况下,性能损失可能是代码效率低下的驱动因素,尤其是在循环中反复调用虚函数时。

这是一篇有些过时的论文,它在嵌入式系统环境中分析了C / C ++的最佳实践:http://www.open-std.org/jtc1/sc22/wg21/docs/ESC_Boston_01_304_paper.pdf

总结:程序员应该理解使用某个构造而不是另一个构造的优缺点。除非你是超级性能驱动的,否则你可能不关心性能问题,应该使用C ++中所有简洁的OO来帮助你的代码尽可能地使用。

答案 11 :(得分:2)

根据我的经验,主要相关的是内联函数的能力。如果您有性能/优化需求,需要内联函数,那么您不能将该函数设置为虚拟,因为它会阻止该功能。否则,你可能不会注意到差异。

答案 12 :(得分:1)

需要注意的一点是:

boolean contains(A element) {
    for (A current: this)
        if (element.equals(current))
            return true;
    return false;
}

可能比这更快:

boolean contains(A element) {
    for (A current: this)
        if (current.equals(equals))
            return true;
    return false;
}

这是因为第一种方法只调用一个函数,而第二种方法可能调用许多不同的函数。这适用于任何语言的任何虚拟功能。

我说&#34;可能&#34;因为这取决于编译器,缓存等。

答案 13 :(得分:0)

使用虚拟功能的性能损失永远不会超过您在设计级别获得的优势。据推测,对虚函数的调用效率比直接调用静态函数低25%。这是因为VMT存在一定程度的间接性。但是,与实际执行函数所花费的时间相比,进行调用所需的时间通常非常小,因此总体性能成本将是不可忽略的,尤其是当前的硬件性能。 此外,编译器有时可以优化并看到不需要虚拟调用并将其编译为静态调用。所以不要担心根据需要使用虚函数和抽象类。

答案 14 :(得分:-1)

我总是质疑自己这一点,特别是因为 - 几年前 - 我也做过这样的测试,比较标准成员方法调用与虚拟成员方法调用的时间,并且对当时的结果非常生气,空洞虚拟呼叫比非虚拟呼叫慢8倍。

今天我必须决定是否使用虚拟函数在我的缓冲类中分配更多内存,在一个性能非常关键的应用程序中,所以我用Google搜索(找到你),最后再次进行测试

// g++ -std=c++0x -o perf perf.cpp -lrt
#include <typeinfo>    // typeid
#include <cstdio>      // printf
#include <cstdlib>     // atoll
#include <ctime>       // clock_gettime

struct Virtual { virtual int call() { return 42; } }; 
struct Inline { inline int call() { return 42; } }; 
struct Normal { int call(); };
int Normal::call() { return 42; }

template<typename T>
void test(unsigned long long count) {
    std::printf("Timing function calls of '%s' %llu times ...\n", typeid(T).name(), count);

    timespec t0, t1;
    clock_gettime(CLOCK_REALTIME, &t0);

    T test;
    while (count--) test.call();

    clock_gettime(CLOCK_REALTIME, &t1);
    t1.tv_sec -= t0.tv_sec;
    t1.tv_nsec = t1.tv_nsec > t0.tv_nsec
        ? t1.tv_nsec - t0.tv_nsec
        : 1000000000lu - t0.tv_nsec;

    std::printf(" -- result: %d sec %ld nsec\n", t1.tv_sec, t1.tv_nsec);
}

template<typename T, typename Ua, typename... Un>
void test(unsigned long long count) {
    test<T>(count);
    test<Ua, Un...>(count);
}

int main(int argc, const char* argv[]) {
    test<Inline, Normal, Virtual>(argc == 2 ? atoll(argv[1]) : 10000000000llu);
    return 0;
}

真的很惊讶它 - 事实上 - 真的根本不重要。 虽然使内联比非虚拟更快,并且它们比虚拟更快,但它通常会带来整个计算机的负载,无论您的缓存是否具有必要的数据,并且您可能能够优化在缓存级别,我认为,这应该由编译器开发人员完成,而不是由应用程序开发人员完成。