虚函数和性能-C ++


125

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


根据我的回答,我建议将其关闭为stackoverflow.com/questions/113830的
Suma,2009年


2
如果您正在执行高性能计算和数字运算,请不要在计算核心中使用任何虚拟性:它肯定会破坏所有性能,并在编译时阻止优化。对于程序的初始化或完成而言,这并不重要。使用接口时,可以根据需要使用虚拟性。
文森特

Answers:


90

一个好的经验法则是:

在您无法证明之前,这不是性能问题。

使用虚拟函数将对性能产生很小的影响,但是不太可能影响应用程序的整体性能。算法和I / O是寻求性能改进的更好地方。

讨论虚函数(以及更多内容)的出色文章是成员函数指针和最快的C ++委托


纯虚拟功能呢?它们是否以任何方式影响性能?只是想知道它们似乎只是在执行实施。
thomthom 2013年

2
@thomthom:是的,纯虚函数和普通虚函数之间没有性能差异。
Greg Hewgill 2013年

168

您的问题使我感到好奇,所以我继续进行一些工作,在我们使用的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),并运行一个循环,将它们彼此相加(Ax = Bx + Cx)1000次。我定义为功能跑到这inlinevirtual和普通函数调用。结果如下:

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

因此,在这种情况下(所有内容都适合缓存),虚拟函数调用的速度比内联调用慢20倍。但这到底是什么意思?遍历循环的每次3 * 4 * 1024 = 12,288调用均引起函数调用(1024个矢量乘以四个分量乘以每个加法运算的三个调用),因此这些时间代表1000 * 12,288 = 12,288,000函数调用。虚拟循环比直接循环花费92毫秒,因此每个调用的额外开销是每个函数7 纳秒

由此我得出结论:是的,虚拟函数比直接函数要慢得多,而,除非您打算每秒将它们调用一千万次,否则没关系。

另请参阅:生成的程序集的比较。


但是,如果多次调用它们,它们通常会比只调用一次便宜。请参阅我无关的博客:phresnel.org/blog,标题为“虚拟功能被认为无害”的帖子,但是当然,这取决于代码路径的复杂性
Sebastian Mach,2009年

22
我的测试测量的是反复调用的一小部分虚拟功能。您的博客文章假定可以通过计算操作来计算代码的时间成本,但这并不总是正确的。在现代处理器上,vfunc的主要成本是分支预测错误导致的管道泡沫。
Crashworks

10
这将是gcc LTO(链接时间优化)的绝佳基准;尝试在启用lto的情况下再次进行编译:gcc.gnu.org/wiki/LinkTimeOptimization并查看20倍因子的情况
lurscher 2011年

1
如果一类具有一个虚函数和一个内联函数,那么非虚方法的性能也会受到影响吗?仅仅是因为类的性质是虚拟的?
thomthom 2014年

4
@thomthom不,虚拟/非虚拟是每个功能的属性。仅当函数被标记为虚函数或覆盖具有虚拟函数的基类时,才需要通过vtable定义函数。您经常会看到类,这些类具有用于公共接口的一组虚函数,然后是许多内联访问器,等等。(从技术上说,这是实现特定的编译器可以使用虚拟ponters甚至打上“内联”的功能,但谁写这样的编译器的人是疯了。)
Crashworks

42

当Objective-C中(其中所有的方法都是虚拟的)是为iPhone和刻着的主要语言的Java是Android的主要语言,我认为这是相当安全的使用在我们的3 GHz的双核塔C ++虚函数。


4
我不确定iPhone是否是高性能代码的一个很好的例子:youtube.com/watch?
v

13
@Crashworks:iPhone根本不是代码示例。这是硬件的一个示例-特别是慢速硬件,这就是我在这里提出的重点。如果这些据说“慢”的语言足以应对功能不足的硬件,那么虚拟功能就不会成为一个大问题。
查克(Chuck)

52
iPhone在ARM处理器上运行。用于iOS的ARM处理器设计用于低MHz和低功耗。CPU上没有用于分支预测的芯片,因此,虚拟函数调用不会因分支预测遗漏任何性能开销。同样,iOS硬件的MHz足够低,以至于高速缓存未命中在从RAM检索数据时不会使处理器停顿300个时钟周期。在较低的MHz下,高速缓存未命中的重要性较小。简而言之,在iOS设备上使用虚拟功能不会带来任何开销,但这是一个硬件问题,不适用于台式机CPU。
HaltingState

4
作为长期使用Java的Java程序员,我想补充一下Java的JIT编译器和运行时优化器具有在预定义数量的循环后在运行时编译,预测甚至内联某些函数的功能。但是我不确定C ++是否在编译和链接时具有这种功能,因为它缺少运行时调用模式。因此,在C ++中,我们可能需要稍加小心。
Alex Suo 2015年

@AlexSuo我不确定你的意思吗?在编译时,C ++当然不能根据运行时可能发生的情况进行优化,因此预测等工作必须由CPU本身来完成……但是好的C ++编译器(如果得到了指导)很久以前就对函数和循环进行了优化。运行。
underscore_d

34

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

当CPU提取新功能的第一条指令且该指令不在高速缓存中时,正常的函数调用可能会生成指令高速缓存未命中。

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

在许多情况下,不必担心两个额外的高速缓存未命中,但是在性能关键代码紧密循环中,它可能会大大降低性能。


6
是的,但是从紧密循环中反复调用的任何代码(或vtable)(当然)很少会遇到缓存未命中的情况。此外,vtable指针通常与被调用方法将访问的对象中的其他数据位于同一缓存行中,因此通常我们只谈论一种额外的缓存未命中。
Qwertie 2011年

5
@Qwertie我认为这不是必须的。循环的主体(如果大于L1缓存)可能会“淘汰” vtable指针,函数指针和后续迭代,因此每次迭代都必须等待L2缓存(或更多)访问
Ghita

30

Agner Fog的“ C ++优化软件”手册的第44页中:

假设函数调用语句始终调用相同版本的虚拟函数,则调用虚拟成员函数所需的时间比调用非虚拟成员函数所需的时间要多几个时钟周期。如果版本更改,则您将遭受10-30个时钟周期的误判惩罚。虚拟函数调用的预测和错误预测规则与switch语句的规则相同...


感谢您的参考。Agner Fog的优化手册是最佳利用硬件的黄金标准。
Arto Bendiken

根据我的回忆和快速搜索-stackoverflow.com/questions/17061967/c-switch-and-jump-tables-我怀疑这对于总是正确的switch。当然,具有完全任意的case值。但是,如果所有cases都是连续的,则编译器也许可以将其优化为跳转表(啊,这让我想起了Z80的美好时光),该表应该(为了更好的用语)应该是恒定时间的。并非我建议尝试用替换vfuncs switch,这很荒谬。;)
underscore_d

7

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

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

我什至唯一想考虑一下虚拟函数对性能的影响的时候就是,如果虚拟函数在模板代码中被大量使用并实例化了,而该模板代码最终贯穿了所有内容。即使那样,我也不会花太多精力!

PS想到了其他“易于使用”的语言-它们的所有方法都是虚拟的,并且如今不再流行。


4
好吧,即使在今天,对于高性能应用程序而言,避免函数调用也很重要。所不同的是,当今的编译器可靠地内联了小函数,因此我们在编写小函数时不会受到速度的损失。至于虚拟功能,智能CPU可以对其进行智能分支预测。我认为,旧计算机速度较慢的事实并不是问题所在,是的,它们速度较慢,但​​那时候我们就知道了,因此我们为它们提供了更小的工作量。在1992年,如果我们播放MP3,我们知道可能必须将一半以上的CPU专用于该任务。
Qwertie 2011年

6
mp3可以追溯到1995年。在92年,我们只有386个,他们无法播放mp3,并且50%的cpu时间假定一个好的多任务操作系统,一个空闲进程和一个抢先式调度程序。当时,在消费者市场上还没有这些产品。从接通电源到故事结束,这是100%。
v.oddou

7

除了执行时间外,还有另一个性能标准。Vtable也占用内存空间,在某些情况下可以避免:ATL使用带有模板的编译时“ 模拟动态绑定获得“静态多态性”的效果,这很难解释;您基本上将派生类作为参数传递给基类模板,因此在编译时,基类“知道”其在每个实例中的派生类。不允许您将多个不同的派生类存储在基本类型的集合中(即运行时多态性),但是从静态的角度来说,如果您想使类Y与已有的模板类X相同,则该类具有此类重写的钩子,您只需要重写您关心的方法,然后获得X类的基本方法,而不必使用vtable。

在具有较大内存占用量的类中,单个vtable指针的开销并不大,但是COM中的某些ATL类非常小,如果永远不会发生运行时多态情况,则值得节省vtable。

另请参阅其他SO问题

顺便说一句,我发现这是一篇有关CPU时间性能方面的文章。


1
这就是所谓的参数多态性
tjysdsg

4

是的,您是对的,如果您对虚拟函数调用的成本感到好奇,则可能会发现这篇文章有趣。


1
链接的文章并不认为虚拟调用非常重要,这可能是分支预测错误。
苏马

4

我看到虚拟函数将成为性能问题的唯一方法是,如果在紧密循环内调用了许多虚拟函数,并且且仅当它们导致页面错误或其他“大量”内存操作发生时,才可以。

尽管就像其他人所说的那样,在现实生活中这几乎永远不会成为您的问题。如果您认为是这样,请运行探查器,进行一些测试,并在尝试对代码进行“非设计”以获得性能优势之前验证是否确实存在问题。


2
在紧密的循环中调用任何内容都可能会使所有代码和数据保持高速缓存中的速度
Greg Rogers

2
是的,但是如果右循环遍历对象列表,则每个对象都可能通过同一函数调用在不同地址处调用虚拟函数。

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;
}

虚拟函数调用的影响在很大程度上取决于情况。如果在函数内部调用很少且工作量很大,则可以忽略不计。

或者,当它是多次重复使用的虚拟呼叫时,同时执行一些简单的操作-可能会很大。


4
与相比,虚拟函数调用的成本很高++ia。所以呢?
Bo Persson '02

2

在我的特定项目中,我至少来回了20次。尽管在代码重用性,清晰度,可维护性和可读性方面可以有一些巨大的收获,但另一方面,虚拟函数的确会影响性能。

在现代笔记本电脑/台式机/平板电脑上性能下降是否会显着...可能不会!但是,在嵌入式系统的某些情况下,性能下降可能是代码效率低下的驱动因素,尤其是在循环调用虚拟函数的情况下。

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

总结一下:程序员应了解使用某种构造而非另一种构造的利弊。除非您是超级性能驱动者,否则您可能根本不在乎性能下降,应该使用C ++中所有精巧的OO工具来帮助使代码尽可能地可用。


2

以我的经验,最重要的是内联函数的能力。如果您有需要内联的性能/优化需求,则必须内联该函数,那么您就不能将其虚拟化,因为这样做会阻止这种情况。否则,您可能不会注意到差异。


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;
}

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

我说“可能”是因为这取决于编译器,缓存等。


0

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


2
永远不会,无论目标计算机多么小?
zumalifeguard

如果您这样The performance penalty of using virtual functions can sometimes be so insignificant that it is completely outweighed by the advantages you get at the design level.sometimes,关键的区别是,不是never
underscore_d

-1

我一直在问自己这个问题,特别是因为-几年前-我还进行了这样的测试,将标准成员方法调用与虚拟方法的调用时间进行了比较,并对当时的结果感到非常生气,因为有空的虚拟调用比非虚拟人慢8倍。

今天,我不得不决定是否在性能非常关键的应用程序中使用虚拟函数在我的缓冲区类中分配更多的内存,因此我用谷歌搜索(找到了你),最后再次进行了测试。

// 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;
}

令我惊讶的是-实际上-不再重要了。内联比非虚拟更快,并且内联比虚拟更快,这通常是有意义的,但这通常涉及整个计算机的负载,无论您的缓存是否具有必需的数据,而且您可能可以进行优化我认为在缓存级别,这应该由编译器开发人员完成,而不是由应用程序开发人员完成。


12
我认为您的编译器很有可能告诉您代码中的虚拟函数调用只能调用Virtual :: call。在这种情况下,它可以内联。即使您没有要求,也没有什么可以阻止编译器内联Normal :: call。因此,我认为这3个操作获得相同时间的可能性很大,因为编译器会为它们生成相同的代码。
Bjarke H. Roune
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.