问题如标题所述:将方法/属性标记为虚拟对性能有何影响?
注意-我假设在通常情况下虚拟方法不会重载;我通常在这里与基类一起工作。
问题如标题所述:将方法/属性标记为虚拟对性能有何影响?
注意-我假设在通常情况下虚拟方法不会重载;我通常在这里与基类一起工作。
Answers:
与直接调用相比,虚拟函数仅具有非常小的性能开销。在低层次上,您基本上是在看数组查找以获取函数指针,然后通过函数指针进行调用。现代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),具体取决于是否针对跳转表进行了优化。
Rico Mariani在他的Performance Tidbits博客中概述了有关绩效的问题,他在博客中指出:
虚方法: 直接调用可以使用虚拟方法吗?人们很多时候都使用虚拟方法来实现将来的可扩展性。可扩展性是一件好事,但它的确付出了代价–确保您已编写了完整的可扩展性故事,并且您对虚拟功能的使用实际上将使您到达所需的位置。例如,有时人们会仔细考虑呼叫站点的问题,但是却不考虑如何创建“扩展”对象。后来,他们意识到(大多数)虚拟功能根本无济于事,他们需要一个完全不同的模型来将“扩展”对象放入系统中。
密封:密封可以是一种将类的多态性限制为仅需要多态性的位置的方法。如果您将完全控制类型,则密封对于性能可能是一件好事,因为它可以直接调用和内联。
基本上,反对虚拟方法的观点是,与直接调用相反,它不允许代码成为内联的候选对象。
在MSDN文章“改善.NET应用程序的性能和可伸缩性”中,进一步阐述了这一点:
考虑虚拟成员的权衡
使用虚拟成员提供可扩展性。如果不需要扩展类设计,请避免使用虚拟成员,因为虚拟表查找会导致虚拟成员的调用成本更高,并且不利于某些运行时性能优化。例如,编译器无法内联虚拟成员。此外,当您允许子类型化时,您实际上会向消费者提交非常复杂的合同,并且将来在尝试升级类时不可避免地会遇到版本问题。
但是,对上述内容的批评来自TDD / BDD阵营(他们希望将方法默认为虚拟),无论如何对性能的影响都可以忽略不计,尤其是当我们可以使用速度更快的计算机时。
通常,虚拟方法只是通过一个功能指针表即可到达实际方法。这意味着一个额外的取消引用和另一个到内存的往返。
尽管成本并非绝对为零,但成本却极低。如果它一定可以帮助您的程序完全具有虚拟功能,那就去做。
为了避免使用v表,拥有一个设计精良的程序要好得多,而不是笨拙的程序,这要好得多,而不是笨拙的程序。
很难肯定地说,因为.NET JIT编译器在某些(许多情况下)可能可以优化开销。
但是,如果它不能优化它,那么我们基本上是在谈论一种额外的指针间接寻址。
也就是说,当您调用非虚拟方法时,您必须
两种情况下的1相同。对于2,使用虚拟方法,您必须改为从对象的vtable中的固定偏移量读取,然后跳转到该位置。这使得分支预测更加困难,并且可能会将某些数据推出CPU缓存。因此,差异并不大,但是如果您将每个函数调用虚拟化,则可以加起来。
它也会抑制优化。编译器可以很容易地内联对非虚函数的调用,因为它确切知道要调用哪个函数。使用虚函数,这有点棘手。一旦确定调用了哪个函数,JIT编译器仍然可以执行此操作,但是要做的工作还很多。
总而言之,它仍然可以累加,特别是在性能至关重要的领域。除非您至少每秒每秒调用数十万次该函数,否则无需担心。
我在C ++中运行了此测试。虚拟函数调用(在3ghz PowerPC上)花费的时间比直接函数调用长7-20纳秒。这意味着,这实际上仅对您计划每秒调用一百万次的函数或较小的函数(其开销可能大于函数本身)重要。(例如,使访问器功能成为虚假习惯的虚假做法是不明智的。)
我还没有用C#进行测试,但是我希望在那里的差异会更少,因为CLR中几乎所有操作都涉及间接操作。
在桌面端,方法是否重载无关紧要,它们会通过方法指针表(虚拟方法表)引起额外的间接访问级别,这意味着在方法调用比较a之前,大约有2个额外的内存通过间接读取。非密封类和非最终方法上的非虚拟方法。
[作为一个有趣的事实,在紧凑型框架版本1.0中,过热更大,因为它不使用虚拟方法表,而只是反射以发现调用虚拟方法时要执行的正确方法。
而且,与非虚拟方法相比,虚拟方法不太可能成为内联或其他优化(如尾部调用)的候选对象。
大致来说,这是方法调用的性能层次结构:
非虚拟方法<虚拟方法<接口方法(在类上)<委托调度<MethodInfo.Invoke <Type.InvokeMember
但是,除非您通过测量证明了这一点,否则各种调度机制对性能的影响都没有关系。