将方法/属性标记为虚拟的性能影响是什么?

时间:2009-02-10 01:49:25

标签: c# performance virtual

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

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

7 个答案:

答案 0 :(得分:136)

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

真正的开销是因为大多数情况下虚拟函数无法内联。 (它们可以是JIT语言,如果VM意识到它们无论如何总是会转到同一个地址。)除了你通过内联获得的加速,内联还可以实现其他几种优化,例如常量折叠,因为调用者可以知道被调用者是怎样的内部工作。对于任何大到不能内联的函数,性能损失可能微不足道。对于可能内联的非常小的函数,当您需要注意虚函数时。

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

答案 1 :(得分:15)

Rico Mariani在他的Performance Tidbits blog中概述了有关表现的问题,他说:

  

虚拟方法:您正在使用吗?   直接调用时的虚方法   会做?很多时候人们一起去   允许未来的虚拟方法   可扩展性。可扩展性是一个   好的,但确实有代价    - 确保您的完全可扩展性   故事已经制定出来并且你的使用   虚拟功能实际上正在进行中   让你到达你需要的地方。   例如,有时人们会想   通过呼叫网站问题然后   不考虑如何“扩展”   将要创建对象。   后来他们意识到(大部分)   虚函数根本没用   他们需要一个完全不同的东西   模型来获取“扩展”对象   进入系统。

     

密封:密封可以是一种方式   限制你的多态性   只是那些网站的类   需要多态性。如果你愿意的话   完全控制类型然后密封   对于表现来说可能是一件好事   因为它可以直接调用和   内联。

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

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

  

考虑虚拟会员的权衡

     

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

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

答案 2 :(得分:11)

通常,虚方法只需通过一个函数表指针即可到达实际方法。这意味着一次额外的引用和一次往返记忆。

虽然成本并非绝对零,但它非常小。 如果它可以帮助您的程序拥有虚拟功能,那么一定要做到。

为了避免使用v-table,最好有一个精心设计的程序,只需要很小的微小的性能,而不是一个笨拙的程序。

答案 3 :(得分:4)

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

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

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

  1. 保存寄存器,生成函数prologue / epilogue以设置参数,复制返回值等。
  2. 跳转到固定且静态知道的地址
  3. 在两种情况下,

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

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

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

答案 4 :(得分:3)

I ran this test in C++。虚函数调用(在3ghz PowerPC上)比直接函数调用长7-20纳秒。这意味着它实际上只对你计划每秒调用一百万次的函数或者对于那么小的函数来说很重要,以至于开销可能比函数本身大。 (例如,将访问者功能虚拟化为盲目习惯可能是不明智的。)

我没有在C#中运行我的测试,但我预计差异会更小,因为CLR中的几乎所有操作都涉及间接。

答案 5 :(得分:2)

从你的标签中,你正在谈论c#。我只能从德尔福的角度回答。我认为它会是类似的。 (我期待这里的负面反馈:))

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

答案 6 :(得分:0)

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

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

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

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

非虚拟方法< Virtual Metods<接口方法(在类上)<代表派遣< MethodInfo.Invoke< Type.InvokeMember

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