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

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

注意 – 我假设虚拟方法在常见情况下不会被重载; 我通常会在这里和基类一起工作。

与直接呼叫相比,虚拟function只有非常小的性能开销。 在低层次上,你基本上是在查看一个数组查找来得到一个函数指针,然后通过一个函数指针进行调用。 现代的CPU甚至可以在分支预测器中合理预测间接函数调用,所以它们通常不会对现代CPUpipe线造成太大的伤害。 在汇编级别,虚拟函数调用转换为如下所示,其中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 

真正的开销是大多数虚拟函数不能被内联。 (如果虚拟机意识到它们始终会到达相同的地址,则它们可以是JIT语言。)除了从内联本身获得的加速之外,内联还启用了其他一些优化,如常量折叠,因为调用者可以知道被调用者在内部工作。 对于那些足够大而不能内联的函数来说,性能影响可能是微不足道的。 对于可能被内联的非常小的函数,那就是当你需要小心虚拟函数的时候。

编辑:要记住的另一件事是,所有的程序需要stream量控制,这是从来没有免费的。 什么会取代你的虚拟function? 一个switch语句? 一系列的if语句? 这些仍然是可能无法预测的分支。 此外,给定一个N路分支,一系列if语句将在O(N)中find正确的path,而一个虚拟函数将在O(1)中find它。 switch语句可能是O(N)或O(1),这取决于它是否被优化为跳转表。

Rico Mariani在他的Performance Tidbits博客中概述了关于performance的问题,他在这里写道:

虚拟方法:直接调用会使用虚拟方法吗? 很多时候人们用虚拟的方法去考虑未来的可扩展性。 可扩展性是一件好事,但它的确有一个价格 – 确保完整的可扩展性故事已经解决,而且使用虚拟function实际上会让你到达需要的地方。 例如,有时候人们通过调用网站问题来思考,但是不考虑如何创build“扩展”对象。 后来他们意识到(大部分)虚拟function根本没有任何帮助,他们需要一个完全不同的模型来将“扩展”对象join到系统中。

密封:密封可以将您的类的多态性限制到需要多态性的那些网站。 如果你能完全控制这个types,那么密封对于性能来说是一件好事,因为它可以直接调用和内联。

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

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

考虑虚拟成员的权衡

使用虚拟成员来提供可扩展性。 如果您不需要扩展您的类devise,请避免使用虚拟成员,因为由于虚拟表查找而致电费用更高,并且会打败某些运行时性能优化。 例如,虚拟成员不能被编译器内联。 另外,当你允许子types化时,你实际上给消费者提供了一个非常复杂的合同,当你试图升级你的课程时,你不可避免地会遇到版本控制问题。

然而,上述批评来自于TDD / BDD阵营(他们希望方法是虚拟的),他们认为性能影响可以忽略不计,特别是当我们能够使用更快的机器时。

通常,一个虚拟方法只需要通过一个表函数指针来实现实际的方法。 这意味着一个额外的解除引用和一个更多的往返记忆。

虽然成本不是绝对零,但是非常小。 如果它帮助你的程序完全具有虚拟function,那么一定要这样做。

有一个精心devise的程序,只是为了避免使用v表而devise的程序要好一点,而不是一个笨拙的程序。

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

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

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

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

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

它也可以禁止优化。 编译器可以很容易地调用非虚函数,因为它确切地知道哪个函数被调用。 使用虚拟function,这有点棘手。 一旦JIT编译器确定调用哪个函数,JIT编译器仍然可以执行此操作,但这需要更多的工作。

总而言之,它仍然可以加起来,特别是在性能至关重要的领域。 但是除非函数至less每秒钟调用数十万次,否则不需要担心。

我用C ++运行这个testing 。 虚函数调用比直接函数调用的时间长7-20纳秒(在3ghz PowerPC上)。 这意味着对于计划每秒调用一百万次的函数,或者对于那些function非常小,以致于开销可能比函数本身更大的函数而言,这确实很重要。 (例如,使访问者function虚拟出来是不明智的。)

我没有在C#中运行我的testing,但是我预计差异会更小,因为几乎CLR中的每个操作都是间接的。

从你的标签,你在谈论C#。 我只能从Delphi的angular度回答。 我认为这将是相似的。 (我期待这里的负面反馈:))

静态方法将在编译时链接。 一个虚拟的方法需要在运行时查找来决定调用哪个方法,所以有一个小的开销。 如果这个方法很小,而且经常被调用,那么这个意义就很重要

在桌面上,这个方法是否被重载并不重要,它们通过方法指针表(虚拟方法表)产生一个额外的间接级别,这意味着在方法调用之前,非密封类和非最终方法的非虚拟方法。

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

同样,虚拟方法也不太可能成为内联或其他优化(如尾调用)而非非虚方法的候选。

粗略地说,这是方法调用的性能层次结构:

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

但是,除非你通过测量certificate,否则各种调度机制的这些性能影响都不重要;)(即便如此,架构的影响,可读性等也可能对select哪一个有很大的影响)