在C ++中,虚函数为什么以及如何变慢?


38

任何人都可以详细解释一下,虚拟表如何工作以及在调用虚拟函数时关联了哪些指针。

如果它们实际上较慢,您是否可以证明虚拟函数执行所需的时间比普通的类方法多?很容易在没有看到某些代码的情况下就无法了解发生的情况。


5
vtable查找正确的方法调用显然要比直接调用该方法花费更长的时间,因为还有很多事情要做。另一个问题是更长的时间,或者该额外时间在您自己的程序范围内是否很重要。 zh.wikipedia.org/wiki/Virtual_method_table
罗伯特·哈维

10
比什么还慢?我看到的代码具有很多switch语句,无法实现动态行为的慢速执行,这仅仅是因为有些程序员听说虚拟函数很慢。
Christopher Creutzig

7
通常,并不是虚拟调用本身很慢,而是编译器无法内联它们。

4
@Kevin Hsu:是的,绝对如此。几乎每当有人告诉您,他们消除了一些“虚拟函数调用开销”后就可以提高速度,如果您研究一下,实际上所有加速的来源将来自优化,而现在这是可能的,因为编译器无法跨之前不确定的呼叫。
2013年

7
即使是能够读取汇编代码的人,也无法准确地预测其在实际CPU执行中的开销。基于台式机的CPU制造商已经进行了数十年的研究,不仅在分支预测方面,而且在价值预测和推测执行方面都进行了投资,其主要原因是掩盖了虚拟功能的延迟。为什么?因为台式机操作系统和软件经常使用它们。(关于移动CPU,我不会这么说。)
rwong

Answers:


55

虚拟方法通常是通过所谓的虚拟方法表(简称vtable)实现的,其中存储了函数指针。这将间接添加到实际的调用中(必须从vtable获取要调用的函数的地址,然后再调用它-而不是直接调用它)。当然,这需要一些时间和更多代码。

但是,它不一定是速度缓慢的主要原因。真正的问题是,编译器(通常/通常)无法知道将调用哪个函数。因此,它无法内联它或执行任何其他此类优化。仅此一项可能会添加十几条毫无意义的指令(准备寄存器,调用然后在随后恢复状态),并可能抑制其他看似无关的优化。此外,如果您通过调用许多不同的实现而疯狂地分支,则遭受与遭受疯狂而通过其他方式分支一样的命中:高速缓存和分支预测器将无济于事,分支将花费比完全可预测的时间更长的时间科。

大但是:这些性能影响通常很小而无所谓。如果您要创建高性能代码并考虑添加一个虚假的函数,该函数会以惊人的频率被调用,那么值得考虑。但是,请记住,与分支的其他方式替换虚函数的调用(if .. elseswitch,函数指针,等等),解决不了根本问题-它很可能会比较慢。问题(如果根本存在)不是虚函数,而是(不必要的)间接。

编辑:通话说明中的差异在其他答案中有所描述。基本上,静态(“正常”)调用的代码是:

  • 复制堆栈上的一些寄存器,以允许被调用的函数使用这些寄存器。
  • 将参数复制到预定义的位置,以便被调用的函数无论在何处都可以找到它们。
  • 推送寄信人地址。
  • 跳转/跳转到函数的代码,该代码是编译时的地址,因此由编译器/链接器以二进制形式进行硬编码。
  • 从预定义的位置获取返回值并还原我们要使用的寄存器。

虚拟调用的功能完全相同,只是在编译时不知道函数地址。相反,有一些说明...

  • 从对象获取vtable指针,该指针指向函数指针数组(函数地址),每个虚拟函数一个。
  • 从vtable中获取正确的函数地址到寄存器中(存储正确函数地址的索引在编译时确定)。
  • 跳转到该寄存器中的地址,而不是跳转到硬编码的地址。

至于分支:分支是指跳转到另一条指令而不仅仅是执行下一条指令的任何事物。其中包括if,,switch各种循环的一部分,函数调用等,有时编译器实现的事情似乎并不以实际需要分支的方式分支。请参阅为什么处理排序数组比未排序数组快?为什么会变慢,CPU如何应对这种变慢以及这不是万能药。


6
@JörgWMittag它们都是解释器,它们仍然比C ++编译器生成的二进制代码慢
Sam

13
@JörgWMittag这些优化的主要目的是在不需要时使(几乎)免费的间接/后期绑定成为可能,因为在这些语言中,每个调用在技术上都是后期绑定。如果您确实确实确实在短时间内从一个地方调用了许多不同的虚拟方法,那么这些优化将无济于事,也无济于事(创建大量的代码都是徒劳的)。C ++家伙对这些优化不是很感兴趣,因为它们处于非常不同的情况下……

10
@JörgWMittag... C ++家伙对这些优化不是很感兴趣,因为它们处于非常不同的情况:AOT编译的vtable方法已经非常快了,很少有调用实际上是虚拟的,许多多态性案例都是早期的-绑定(通过模板),因此可以修改AOT优化。最后,在做这些优化自适应(而不是在编译时只是投机)需要运行时代码生成,介绍头痛。JIT编译器已经出于其他原因解决了这些问题,因此他们不介意,但是AOT编译器希望避免这种情况。

3
好答案,+ 1。但是要注意的一件事是,有时在编译时就知道分支的结果,例如,当您编写需要支持不同用途的框架类时,但是一旦应用程序代码与这些类交互,则特定用途就已经知道了。在这种情况下,虚拟函数的替代方案可以是C ++模板。一个很好的例子就是CRTP,它可以在没有任何vtable的情况下模拟虚拟函数的行为:en.wikipedia.org/wiki/Curiously_recurring_template_pattern
DXM

3
@詹姆斯你有意思。我想说的是:任何间接方法都具有相同的问题,这与无关virtual

23

这是分别从虚拟函数调用和非虚拟调用中得到的一些实际反汇编的代码:

mov    -0x8(%rbp),%rax
mov    (%rax),%rax
mov    (%rax),%rax
callq  *%rax

callq  0x4007aa

您可以看到虚拟调用需要另外三个指令来查找正确的地址,而非虚拟调用的地址可以被编译。

但是,请注意,在大多数情况下,额外的查找时间可以忽略不计。在查找时间很长的情况下(例如在循环中),通常可以通过在循环之前执行前三个指令来缓存值。

查找时间变得很重要的另一种情况是,如果您有一个对象集合,并且正在通过循环调用每个对象的虚函数。但是,在这种情况下,您将需要一些方法来选择无论如何都要调用哪个函数,并且虚拟表查找与任何方法一样好。事实上,由于虚函数表查找代码是如此广泛的应用是高度优化的,所以试图解决它具备手动导致一个很好的机会,更糟糕的表现。


1
需要了解的是,在几乎所有情况下,vtable查找和间接调用对被调用方法的总运行时间的影响都可以忽略不计。
John R. Strohm 2013年

11
@ JohnR.Strohm一个人的微不足道是另一个人的瓶颈
詹姆斯

1
-0x8(%rbp)。哦,我的... AT&T语法。
Abyx

三个附加说明 ”否,只有两个:加载vptr和加载函数指针
curiousguy 2015年

@curiousguy实际上是另外三个说明。您忘记了总是在指针上调用虚拟方法,因此必须首先将指针加载到寄存器中。综上所述,第一步是将指针变量保存的地址加载到寄存器%rax中,然后根据该寄存器中的地址,将该地址上的vtpr加载到寄存器%rax中,然后根据该地址注册,将要调用的方法的地址加载到%rax中,然后调用q *%rax!。
加布是好人

18

比什么还慢?

虚函数解决了直接函数调用无法解决的问题。通常,您只能比较两个计算相同内容的程序。“这种光线追踪器比编译器快”是没有道理的,并且该原理甚至可以推广到诸如单个函数或编程语言构造之类的小事情上。

如果您不使用虚函数根据基准来动态切换到一段代码,例如对象的类型,那么您将不得不使用其他内容(例如switch语句)来完成相同的事情。别的东西都有自己的开销,加上对程序组织的影响,这会影响程序的可维护性和全局性能。

请注意,在C ++中,对虚函数的调用并不总是动态的。当对确切类型已知的对象进行调用时(由于该对象不是指针或引用,或者因为可以静态推断其类型),这些调用只是常规成员函数调用。这不仅意味着没有调度开销,而且这些调用可以以与普通调用相同的方式内联。

换句话说,当虚拟函数不需要虚拟调度时,您的C ++编译器就可以工作,因此通常没有理由担心它们相对于非虚拟函数的性能。

新增内容:此外,我们一定不能忘记共享库。如果您使用的是共享库中的类,则对普通成员函数的调用将不会像那样简单地成为一个很好的指令序列callq 0x4007aa。它必须经历一些麻烦,例如通过“程序链接表”或某些此类结构进行间接访问。因此,共享库间接调用可以在某种程度上(如果不是完全的话)平衡(真正间接)虚拟调用和直接调用之间的成本差异。因此,在进行虚函数折衷的推理时必须考虑程序的构建方式:目标对象的类是否整体链接到进行调用的程序中。


4
“比什么还慢?” -如果您将不需要的方法设为虚拟,那么您将拥有很好的比较材料。
tdammers

2
感谢您指出对虚函数的调用并非总是动态的。此处的所有其他响应都使得无论在什么情况下,声明一个虚拟函数都意味着自动性能下降。
Syndog

12

因为虚拟通话相当于

res_t (*foo)(arg_t);
foo = (obj->vtable[foo_offset]);
foo(obj,args)

在使用非虚拟函数的情况下,编译器可以对第一行进行常量折叠,这是对附加内容的取消引用,并且将动态调用转换为仅静态调用

这也使它可以内联函数(具有所有适当的优化结果)

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.