68

问题如标题中所述:将方法/属性标记为虚拟的性能影响是什么?

注意 - 我假设虚拟方法在常见情况下不会被重载;我通常会在这里使用基类。

4

7 回答 7

150

与直接调用相比,虚函数只有很小的性能开销。在低级别,您基本上是在查看数组查找以获取函数指针,然后通过函数指针进行调用。现代 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),这取决于它是否优化为跳转表。

于 2009-02-10T01:58:16.740 回答
16

Rico Mariani 在他的Performance Tidbits 博客中概述了有关性能的问题,他在其中指出:

虚拟方法: 当直接调用可以使用时,您是否使用虚拟方法?很多时候,人们使用虚拟方法来实现未来的可扩展性。可扩展性是一件好事,但它确实是有代价的——确保你的完整的可扩展性故事已经完成,并且你对虚函数的使用实际上会让你到达你需要的地方。例如,有时人们会考虑调用站点的问题,但不会考虑如何创建“扩展”对象。后来他们意识到(大部分)虚函数根本没有帮助,他们需要一个完全不同的模型来将“扩展”对象引入系统。

密封:密封可以将类的多态性限制在需要多态性的那些位点。如果您将完全控制类型,那么密封对于性能来说可能是一件好事,因为它支持直接调用和内联。

基本上反对虚拟方法的论点是它不允许代码成为内联的候选者,而不是直接调用。

在 MSDN 文章Improvement .NET Application Performance and Scalability中,进一步阐述了这一点:

考虑虚拟会员的权衡

使用虚拟成员提供可扩展性。如果您不需要扩展您的类设计,请避免使用虚拟成员,因为由于虚拟表查找,调用它们的成本更高,并且它们会破坏某些运行时性能优化。例如,编译器不能内联虚拟成员。此外,当您允许子类型化时,您实际上向消费者提供了一个非常复杂的合同,并且当您将来尝试升级您的类时,您不可避免地会遇到版本控制问题。

然而,对上述内容的批评来自 TDD/BDD 阵营(他们希望方法默认为虚拟),他们认为无论如何性能影响都可以忽略不计,尤其是当我们可以访问速度更快的机器时。

于 2009-02-10T02:05:51.370 回答
11

通常,虚拟方法只需通过一个函数表指针即可到达实际方法。这意味着一个额外的取消引用和一个更多的内存往返。

虽然成本不是绝对零,但它非常小。如果它有助于您的程序具有虚拟功能,那么一定要这样做。

拥有一个设计精良、性能影响很小、很小、很小的程序要比仅仅为了避免 v-table 而设计一个笨拙的程序要好得多。

于 2009-02-10T02:01:00.663 回答
4

很难确定,因为 .NET JIT 编译器可能能够在某些(很多?)情况下优化开销。

但如果它不优化它,我们基本上是在谈论一个额外的指针间接。

也就是说,当你调用一个非虚方法时,你必须

  1. 保存寄存器,生成函数 prologue/epilogue 以设置参数,复制返回值等。
  2. 跳转到一个固定的、静态已知的地址

1 在这两种情况下是相同的。至于 2,使用虚拟方法,您必须改为从对象的 vtable 中的固定偏移量读取,然后跳转到该点的任何位置。这使得分支预测更加困难,并且可能会将一些数据推出 CPU 缓存。所以差异不是很大,但如果你让每个函数调用都是虚拟的,它可以加起来。

它还可以抑制优化。编译器可以轻松地内联对非虚拟函数的调用,因为它确切地知道调用了哪个函数。使用虚函数,这有点棘手。一旦确定调用了哪个函数,JIT 编译器可能仍然能够做到这一点,但工作量要大得多。

总而言之,它仍然可以加起来,尤其是在性能关键领域。但这不是您需要担心的事情,除非该函数每秒至少调用几十万次。

于 2009-02-10T02:04:21.607 回答
3

从你的标签,你说的是 c#。我只能从德尔福的角度回答。我认为这将是相似的。(我在这里期待负面反馈:))

静态方法将在编译时链接。虚拟方法需要在运行时查找来决定调用哪个方法,因此开销很小。只有当方法很小并且经常调用时,它才有意义。

于 2009-02-10T01:56:46.913 回答
3

我在 C++ 中运行了这个测试。虚函数调用(在 3ghz PowerPC 上)比直接函数调用要长 7-20 纳秒。这意味着它只对您计划每秒调用一百万次的函数,或者对于那些很小以至于开销可能大于函数本身的函数才重要。(例如,出于盲目的习惯将访问器函数设为虚拟可能是不明智的。)

我还没有在 C# 中运行我的测试,但我希望那里的差异会更小,因为 CLR 中的几乎每个操作都涉及间接操作。

于 2009-02-10T02:05:37.143 回答
0

在桌面端,方法是否重载并不重要,它们会通过方法指针表(虚拟方法表)产生额外的间接级别,这意味着在方法调用比较之前通过间接读取大约 2 个额外的内存非密封类和非最终方法上的非虚方法。

[有趣的事实是,在紧凑框架版本 1.0 上,过热更大,因为它不使用虚拟方法表,而只是通过反射来发现调用虚拟方法时要执行的正确方法。]

此外,与非虚拟方法相比,虚拟方法不太可能成为内联或其他优化(如尾调用)的候选者。

大致这是方法调用的性能层次结构:

非虚方法 < Virtual Metods < 接口方法(在类上) < 委托调度 < MethodInfo.Invoke < Type.InvokeMember

但是,除非您通过测量来证明,否则各种调度机制的这些性能影响都无关紧要;)(即使这样,架构影响、可读性等也可能对选择哪一个有很大的影响)

于 2009-02-10T02:00:14.550 回答