当前位置:  开发笔记 > 编程语言 > 正文

虚函数和性能 - C++

如何解决《虚函数和性能-C++》经验,为你挑选了7个好方法。

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



1> Crashworks..:

你的问题让我很好奇,所以我继续在我们使用的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次.我定义为功能跑到这inline,virtual和普通函数调用.结果如下:

内联:8ms(每通话0.65ns)

直接:68ms(每通话5.53ns)

虚拟:160毫秒(每通话13ns)

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

由此我得出结论:是的,虚函数比直接函数慢得多,,除非你计划每秒调用它们一千万次,否则没关系.

另请参见:生成的程序集的比较.


我的测试测量了一组反复调用的虚函数.您的博客文章假设代码的时间成本可以通过计算操作来衡量,但并非总是如此; 现代处理器上vfunc的主要成本是由分支错误预测引起的管道泡沫.
这将是gcc LTO(链接时间优化)的一个很好的基准; 尝试使用lto启用再次编译:http://gcc.gnu.org/wiki/LinkTimeOptimization,看看20x因素会发生什么
@thomthom不,虚拟/非虚拟是每个功能的属性.如果函数被标记为虚拟或者它覆盖了将其作为虚拟的基类,则只需要通过vtable定义函数.您经常会看到具有一组虚拟函数的类用于公共接口,然后是许多内联访问器等等.(从技术上讲,这是特定于实现的,即使对于标记为"内联"的函数,编译器也可以使用虚拟ponters,但是编写这样的编译器的人会疯了.)

2> Greg Hewgill..:

一个好的经验法则是:

在你证明这一点之前,这不是一个性能问题.

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

一篇关于虚函数(以及更多)的优秀文章是成员函数指针和最快可能的C++代表.


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

3> Chuck..:

当Objective-C(其中所有方法都是虚拟的)是iPhone 的主要语言时,JavaJava的主要语言,我认为在我们的3 GHz双核塔上使用C++虚拟功能是相当安全的.


iPhone在ARM处理器上运行.用于iOS的ARM处理器专为低MHz和低功耗而设计.在CPU上没有用于分支预测的硅,因此没有来自虚拟函数调用的分支预测未命中的性能开销.此外,iOS硬件的MHz足够低,以至于当从RAM中检索数据时,高速缓存未命中不会使处理器停止300个时钟周期.在较低的MHz下,高速缓存未命中不太重要.简而言之,在iOS设备上使用虚拟功能没有任何开销,但这是硬件问题,不适用于台式机CPU.
@Crashworks:iPhone根本不是代码的例子.这是硬件的一个例子 - 特别是*慢硬件*,这是我在这里提出的观点.如果这些据说"慢"的语言对于功能不足的硬件来说足够好,那么虚函数就不会是一个大问题.
我不确定iPhone是高性能代码的好例子:http://www.youtube.com/watch?v = PDk2cJpSXLg
作为Java程序员的新成员,我想补充一点,Java的JIT编译器和运行时优化器能够在预定义数量的循环之后在运行时编译,预测甚至内联一些函数.但是我不确定C++在编译和链接时是否具有此类功能,因为它缺少运行时调用模式.因此在C++中我们可能需要稍微小心一些.

4> 小智..:

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

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

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

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


是的,但是从紧密循环中重复调用的任何代码(或vtable)(当然)很少会遇到缓存未命中.此外,vtable指针通常与被调用方法将访问的对象中的其他数据位于同一缓存行中,因此我们通常只讨论一个额外的缓存未命中.
@Qwertie我不认为这是必要的.循环体(如果大于L1缓存)可以"退出"vtable指针,函数指针和后续迭代将不得不等待每次迭代的L2缓存(或更多)访问

5> Boojum..:

从Agner Fog的"在C++中优化软件"手册的第44页开始:

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



6> gbjbaanb..:

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

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

我唯一想到考虑虚拟函数性能影响的时候,就是它被大量使用并在模板化代码中实例化,最终贯穿始终.即便如此,我也不会花太多精力!

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


mp3的历史可以追溯到1995年.在92年,我们几乎没有386,他们无法播放mp3,50%的cpu时间假定一个好的多任务操作系统,一个空闲进程和一个抢占式调度程序.当时在消费者市场上都不存在这种情况.从动力开启的那一刻开始,这是100%的故事.
好吧,即使在今天,避免函数调用对于高性能应用程序也很重要.不同的是,今天的编译器可靠地内联小函数,因此我们不会因编写小函数而受到速度惩罚.对于虚拟功能,智能CPU可以对它们进行智能分支预测.我认为旧计算机速度较慢的事实并非真正的问题 - 是的,它们的速度要慢得多,但当时我们知道这一点,所以我们给它们的工作量要小得多.1992年,如果我们播放MP3,我们知道我们可能不得不将超过一半的CPU用于该任务.

7> Jason S..:

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

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

另见其他SO问题.

顺便说一下,我发现这篇文章讨论了CPU时间性能方面的问题.

推荐阅读
雨天是最美
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有