问题如标题所述:将方法/属性标记为虚拟的性能影响是什么?
注意 - 我假设虚拟方法在常见情况下不会过载; 我通常会在这里使用基类.
与直接调用相比,虚拟功能仅具有非常小的性能开销.在较低级别,您基本上是在查看数组查找以获取函数指针,然后通过函数指针进行调用.现代CPU甚至可以在其分支预测器中合理地预测间接函数调用,因此它们通常不会太严重地损害现代CPU流水线.在汇编级别,虚函数调用转换为类似以下内容,其中I
是任意立即值.
MOV EAX, [EBP + I] ; Move pointer to class instance into register MOV EBX, [EAX] ; Move vtbl pointer into register. CALL [EBX + I] ; Call function
比.以下为直接函数调用:
CALL I ; Call function directly
真正的开销是因为大多数情况下虚拟函数无法内联.(它们可以是JIT语言,如果VM意识到它们无论如何总是会转到同一个地址.)除了你通过内联获得的加速,内联还可以实现其他几种优化,例如常量折叠,因为调用者可以知道被调用者是怎样的内部工作.对于任何大到不能内联的函数,性能损失可能微不足道.对于可能内联的非常小的函数,当您需要注意虚函数时.
编辑:要记住的另一件事是所有程序都需要流量控制,这绝不是免费的.什么会取代你的虚拟功能?转换声明?一系列if语句?这些仍然是可能无法预测的分支.此外,给定N路分支,一系列if语句将在O(N)中找到正确的路径,而虚函数将在O(1)中找到它.switch语句可以是O(N)或O(1),这取决于它是否针对跳转表进行了优化.
Rico Mariani在他的Performance Tidbits博客中概述了有关性能的问题,他说:
虚方法: 直接调用时,您是否使用虚方法?很多时候,人们使用虚拟方法来实现未来的可扩展性.可扩展性是一件好事,但它确实付出了代价 - 确保您的完整可扩展性故事得到解决,并且您对虚拟功能的使用实际上将使您到达您需要的位置.例如,有时人们会考虑调用站点问题,但是不考虑如何创建"扩展"对象.后来他们意识到(大部分)虚拟功能根本没有帮助,他们需要一个完全不同的模型来将"扩展"对象引入系统.
密封:密封可以是将类的多态性限制为仅需要多态性的站点的方法.如果您将完全控制类型,那么密封对于性能来说是一件好事,因为它可以实现直接调用和内联.
基本上反对虚方法的论点是它不允许代码成为内联的候选者,而不是直接调用.
在MSDN文章" 提高.NET应用程序性能和可伸缩性"中,进一步阐述了这一点:
考虑虚拟会员的权衡
使用虚拟成员提供可扩展性.如果您不需要扩展类设计,请避免使用虚拟成员,因为由于虚拟表查找而调用它们的成本更高,并且它们会使某些运行时性能优化失败.例如,编译器无法内联虚拟成员.此外,当您允许子类型化时,您实际上向消费者提供了一份非常复杂的合同,并且当您将来尝试升级您的课程时,您不可避免地会遇到版本问题.
然而,对上述内容的批评来自TDD/BDD阵营(他们希望方法默认为虚拟),认为性能影响无论如何都可以忽略不计,特别是当我们可以访问速度更快的机器时.
通常,虚拟方法只需通过一个函数表指针即可到达实际方法.这意味着一次额外的解除引用和一次往返内存.
虽然成本并非绝对零,但它非常小.如果它可以帮助您的程序拥有虚拟功能,那么一定要做到这一点.
为了避免使用v-table,最好是拥有一个精心设计的程序,只需要很小的微小的性能,而不是一个笨拙的程序.