问题如标题中所述:将方法/属性标记为虚拟的性能影响是什么?
注意 - 我假设虚拟方法在常见情况下不会被重载;我通常会在这里使用基类。
与直接调用相比,虚函数只有很小的性能开销。在低级别,您基本上是在查看数组查找以获取函数指针,然后通过函数指针进行调用。现代 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
真正的开销在于虚拟函数在大多数情况下无法内联。(如果 VM 意识到它们总是会去同一个地址,它们可以是 JIT 语言。)除了内联本身获得的加速之外,内联还支持其他一些优化,例如常量折叠,因为调用者可以知道被调用者如何在内部工作。对于大到不能被内联的函数,对性能的影响可能可以忽略不计。对于可能被内联的非常小的函数,这时您需要注意虚函数。
编辑:要记住的另一件事是,所有程序都需要流量控制,而这绝不是免费的。什么会取代你的虚拟功能?一个switch语句?一系列 if 语句?这些仍然是可能无法预测的分支。此外,给定一个 N 路分支,一系列 if 语句将在 O(N) 中找到正确的路径,而虚函数将在 O(1) 中找到它。switch 语句可能是 O(N) 或 O(1),这取决于它是否优化为跳转表。
Rico Mariani 在他的Performance Tidbits 博客中概述了有关性能的问题,他在其中指出:
虚拟方法: 当直接调用可以使用时,您是否使用虚拟方法?很多时候,人们使用虚拟方法来实现未来的可扩展性。可扩展性是一件好事,但它确实是有代价的——确保你的完整的可扩展性故事已经完成,并且你对虚函数的使用实际上会让你到达你需要的地方。例如,有时人们会考虑调用站点的问题,但不会考虑如何创建“扩展”对象。后来他们意识到(大部分)虚函数根本没有帮助,他们需要一个完全不同的模型来将“扩展”对象引入系统。
密封:密封可以将类的多态性限制在需要多态性的那些位点。如果您将完全控制类型,那么密封对于性能来说可能是一件好事,因为它支持直接调用和内联。
基本上反对虚拟方法的论点是它不允许代码成为内联的候选者,而不是直接调用。
在 MSDN 文章Improvement .NET Application Performance and Scalability中,进一步阐述了这一点:
考虑虚拟会员的权衡
使用虚拟成员提供可扩展性。如果您不需要扩展您的类设计,请避免使用虚拟成员,因为由于虚拟表查找,调用它们的成本更高,并且它们会破坏某些运行时性能优化。例如,编译器不能内联虚拟成员。此外,当您允许子类型化时,您实际上向消费者提供了一个非常复杂的合同,并且当您将来尝试升级您的类时,您不可避免地会遇到版本控制问题。
然而,对上述内容的批评来自 TDD/BDD 阵营(他们希望方法默认为虚拟),他们认为无论如何性能影响都可以忽略不计,尤其是当我们可以访问速度更快的机器时。
通常,虚拟方法只需通过一个函数表指针即可到达实际方法。这意味着一个额外的取消引用和一个更多的内存往返。
虽然成本不是绝对零,但它非常小。如果它有助于您的程序具有虚拟功能,那么一定要这样做。
拥有一个设计精良、性能影响很小、很小、很小的程序要比仅仅为了避免 v-table 而设计一个笨拙的程序要好得多。
很难确定,因为 .NET JIT 编译器可能能够在某些(很多?)情况下优化开销。
但如果它不优化它,我们基本上是在谈论一个额外的指针间接。
也就是说,当你调用一个非虚方法时,你必须
1 在这两种情况下是相同的。至于 2,使用虚拟方法,您必须改为从对象的 vtable 中的固定偏移量读取,然后跳转到该点的任何位置。这使得分支预测更加困难,并且可能会将一些数据推出 CPU 缓存。所以差异不是很大,但如果你让每个函数调用都是虚拟的,它可以加起来。
它还可以抑制优化。编译器可以轻松地内联对非虚拟函数的调用,因为它确切地知道调用了哪个函数。使用虚函数,这有点棘手。一旦确定调用了哪个函数,JIT 编译器可能仍然能够做到这一点,但工作量要大得多。
总而言之,它仍然可以加起来,尤其是在性能关键领域。但这不是您需要担心的事情,除非该函数每秒至少调用几十万次。
从你的标签,你说的是 c#。我只能从德尔福的角度回答。我认为这将是相似的。(我在这里期待负面反馈:))
静态方法将在编译时链接。虚拟方法需要在运行时查找来决定调用哪个方法,因此开销很小。只有当方法很小并且经常调用时,它才有意义。
我在 C++ 中运行了这个测试。虚函数调用(在 3ghz PowerPC 上)比直接函数调用要长 7-20 纳秒。这意味着它只对您计划每秒调用一百万次的函数,或者对于那些很小以至于开销可能大于函数本身的函数才重要。(例如,出于盲目的习惯将访问器函数设为虚拟可能是不明智的。)
我还没有在 C# 中运行我的测试,但我希望那里的差异会更小,因为 CLR 中的几乎每个操作都涉及间接操作。
在桌面端,方法是否重载并不重要,它们会通过方法指针表(虚拟方法表)产生额外的间接级别,这意味着在方法调用比较之前通过间接读取大约 2 个额外的内存非密封类和非最终方法上的非虚方法。
[有趣的事实是,在紧凑框架版本 1.0 上,过热更大,因为它不使用虚拟方法表,而只是通过反射来发现调用虚拟方法时要执行的正确方法。]
此外,与非虚拟方法相比,虚拟方法不太可能成为内联或其他优化(如尾调用)的候选者。
大致这是方法调用的性能层次结构:
非虚方法 < Virtual Metods < 接口方法(在类上) < 委托调度 < MethodInfo.Invoke < Type.InvokeMember
但是,除非您通过测量来证明,否则各种调度机制的这些性能影响都无关紧要;)(即使这样,架构影响、可读性等也可能对选择哪一个有很大的影响)