将方法/属性标记为虚拟对性能有何影响?


Answers:


147

与直接调用相比,虚拟函数仅具有非常小的性能开销。在低层次上,您基本上是在看数组查找以获取函数指针,然后通过函数指针进行调用。现代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

VS. 以下是直接函数调用:

CALL I  ;  Call function directly

真正的开销来自于大多数情况下无法内联虚拟函数。(如果VM意识到无论如何它们总是会使用相同的地址,则它们可以使用JIT语言。)除了可以通过内联自身获得加速之外,内联还可以实现其他一些优化,例如恒定折叠,因为调用方可以知道被调用方的方式。在内部工作。对于足够大以至于无法内联的函数,性能影响可能会忽略不计。对于可能内联的非常小的函数,那时候您需要注意虚拟函数。

编辑:要记住的另一件事是,所有程序都需要流控制,而这永远都不是免费的。什么会取代您的虚拟功能?切换语句?一系列的if语句?这些仍然是可能无法预测的分支。此外,给定N路分支,一系列if语句将在O(N)中找到正确的路径,而虚函数将在O(1)中找到它。switch语句可以是O(N)或O(1),具体取决于是否针对跳转表进行了优化。


11
我一段时间以来见过的最有信息的帖子之一,我以前并不了解,但仍然很容易理解。
克里斯·马里西克

很好的答案-非常感谢。由于我不打算在一个非常紧密的循环中调用我的方法,因此我不必为此担心太多。谢谢您的帮助=)
Erik Forbes,2009年

1
Anton Ertl维护了一个有用的基准页面(带有源代码),其中包括针对各种处理器的直接函数调用成本与间接函数分配成本:complang.tuwien.ac.at/forth/threading
Arto Bendiken

还需要注意的是switch语句可以使用二进制搜索来实现,在这种情况下,其复杂度为O(log N),而不是O(N):stackoverflow.com/questions/2596320/...
阿尔托Bendiken

16

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

虚方法: 直接调用可以使用虚拟方法吗?人们很多时候都使用虚拟方法来实现将来的可扩展性。可扩展性是一件好事,但它的确付出了代价–确保您已编写了完整的可扩展性故事,并且您对虚拟功能的使用实际上将使您到达所需的位置。例如,有时人们会仔细考虑呼叫站点的问题,但是却不考虑如何创建“扩展”对象。后来,他们意识到(大多数)虚拟功能根本无济于事,他们需要一个完全不同的模型来将“扩展”对象放入系统中。

密封:密封可以是一种将类的多态性限制为仅需要多态性的位置的方法。如果您将完全控制类型,则密封对于性能可能是一件好事,因为它可以直接调用和内联。

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

在MSDN文章“改善.NET应用程序的性能和可伸缩性”中,进一步阐述了这一点:

考虑虚拟成员的权衡

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

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


11

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

尽管成本并非绝对为零,但成本却极低。如果它一定可以帮助您的程序完全具有虚拟功能,那就去做。

为了避免使用v表,拥有一个设计精良的程序要好得多,而不是笨拙的程序,这要好得多,而不是笨拙的程序。


4
虚拟函数调用的最大代价不是从vtable加载指针,而是管道清除了错误分支导致的结果(并且vjump通常被错误预测)。该时间可以与管道本身一样长。对于非常频繁调用的函数,它将加起来。
Crashworks

@Crashworks:有趣的想法。但是,这似乎与另一个问题(stackoverflow.com/questions/10757167/…)中的某些评论相矛盾。愿意发表评论吗?(我不知道谁是对的...只是想尽我所能吸收所有信息)
abelenky 2012年

2
该问题中的大多数答案都是错误的,尤其是对于有序处理器,因为间接分支通常会被错误地预测。
Crashworks 2012年

现在,我不知道这是C#特异的,因为这违背了所有一般C ++性能知识
邮政自

@Crashworks确实是,不是我们有间接的,而是缓存未命中,这会导致性能下降

4

很难肯定地说,因为.NET JIT编译器在某些(许多情况下)可能可以优化开销。

但是,如果它不能优化它,那么我们基本上是在谈论一种额外的指针间接寻址。

也就是说,当您调用非虚拟方法时,您必须

  1. 保存寄存器,生成函数序言/结尾以设置参数,复制返回值等。
  2. 跳转到固定且静态已知的地址

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

它也会抑制优化。编译器可以很容易地内联对非虚函数的调用,因为它确切知道要调用哪个函数。使用虚函数,这有点棘手。一旦确定调用了哪个函数,JIT编译器仍然可以执行此操作,但是要做的工作还很多。

总而言之,它仍然可以累加,特别是在性能至关重要的领域。除非您至少每秒每秒调用数十万次该函数,否则无需担心。


3

从标记中,您正在谈论c#。我只能从Delphi的角度回答。我认为会类似。(我期望这里是负面反馈:))

静态方法将在编译时链接。虚拟方法需要在运行时查找以确定要调用的方法,因此开销很小。仅当该方法较小且经常调用时才有意义。


3

我在C ++中运行了此测试。虚拟函数调用(在3ghz PowerPC上)花费的时间比直接函数调用长7-20纳秒。这意味着,这实际上仅对您计划每秒调用一百万次的函数或较小的函数(其开销可能大于函数本身)重要。(例如,使访问器功能成为虚假习惯的虚假做法是不明智的。)

我还没有用C#进行测试,但是我希望在那里的差异会更少,因为CLR中几乎所有操作都涉及间接操作。


“解释的运行时”?嗯,人们,.Net甚至不是真正的虚拟机,而且9年后仍然有人认为.Net被解释为.... pff
Pop Catalin

不,您计划调用数百万次函数的性能只会受到少许影响。当您更改调用的函数并因此造成高速缓存未命中时,命中率最高的地方是
发布自我

0

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

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

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

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

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

但是,除非您通过测量证明了这一点,否则各种调度机制对性能的影响都没有关系。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.