在我的类设计中,我广泛使用抽象类和虚函数.我感觉虚拟功能会影响性能.这是真的?但我认为这种性能差异并不明显,看起来我正在做过早的优化.对?
你的问题让我很好奇,所以我继续在我们使用的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
函数调用.虚拟循环比直接循环花费了92ms,因此每个函数的额外开销是每个函数7 纳秒.
由此我得出结论:是的,虚函数比直接函数慢得多,不,除非你计划每秒调用它们一千万次,否则没关系.
另请参见:生成的程序集的比较.
一个好的经验法则是:
在你证明这一点之前,这不是一个性能问题.
虚函数的使用对性能影响很小,但不太可能影响应用程序的整体性能.寻找性能改进的更好地方是算法和I/O.
一篇关于虚函数(以及更多)的优秀文章是成员函数指针和最快可能的C++代表.
当Objective-C(其中所有方法都是虚拟的)是iPhone 的主要语言时,Java是Java的主要语言,我认为在我们的3 GHz双核塔上使用C++虚拟功能是相当安全的.
在性能非常关键的应用程序(如视频游戏)中,虚拟函数调用可能太慢.使用现代硬件,最大的性能问题是缓存未命中.如果数据不在缓存中,则在可用之前可能需要数百个周期.
当CPU获取新函数的第一条指令并且它不在缓存中时,正常函数调用可以生成指令缓存未命中.
虚函数调用首先需要从对象加载vtable指针.这可能导致数据缓存未命中.然后它从vtable加载函数指针,这可能导致另一个数据缓存未命中.然后它调用可能导致指令高速缓存未命中的函数,如非虚函数.
在许多情况下,两个额外的缓存未命中并不是一个问题,但在性能关键代码的紧密循环中,它可以大大降低性能.
从Agner Fog的"在C++中优化软件"手册的第44页开始:
如果函数调用语句总是调用相同版本的虚函数,则调用虚拟成员函数所花费的时间比调用非虚函数成员函数所花费的时间长几个时钟周期.如果版本发生变化,那么您将获得10到30个时钟周期的错误预测惩罚.虚函数调用的预测和误预测规则与switch语句相同......
绝对.当计算机以100Mhz运行时,这是一个问题,因为每个方法调用都需要在vtable调用之前查找vtable.但是今天......在3Ghz CPU上有一级缓存,内存比第一台计算机多?一点也不.从主RAM分配内存将花费您比所有功能都是虚拟的更多时间.
它就像旧的,过去人们说结构化编程很慢,因为所有代码都被分成了函数,每个函数都需要堆栈分配和函数调用!
我唯一想到考虑虚拟函数性能影响的时候,就是它被大量使用并在模板化代码中实例化,最终贯穿始终.即便如此,我也不会花太多精力!
PS想到其他"易于使用"的语言 - 他们所有的方法都是虚拟的,他们现在不会爬行.
除了执行时间之外还有另一个性能标准.Vtable也占用了内存空间,在某些情况下可以避免:ATL使用编译时" 模拟动态绑定 "和模板获得"静态多态性"的效果,这有点难以解释; 您基本上将派生类作为参数传递给基类模板,因此在编译时基类"知道"它在每个实例中的派生类.不允许您在基类型集合(即运行时多态)中存储多个不同的派生类,但是从静态意义上说,如果您想创建一个类Y,它与已存在的模板类X相同,它具有这种覆盖的钩子,你只需要覆盖你关心的方法,然后你得到类X的基本方法,而不必有一个vtable.
在具有大内存占用的类中,单个vtable指针的成本并不多,但COM中的某些ATL类非常小,如果运行时多态性情况永远不会发生,那么值得节省vtable.
另见其他SO问题.
顺便说一下,我发现这篇文章讨论了CPU时间性能方面的问题.