任何人都可以详细解释一下,虚拟表如何工作以及在调用虚拟函数时关联了哪些指针。
如果它们实际上较慢,您是否可以证明虚拟函数执行所需的时间比普通的类方法多?很容易在没有看到某些代码的情况下就无法了解发生的情况。
任何人都可以详细解释一下,虚拟表如何工作以及在调用虚拟函数时关联了哪些指针。
如果它们实际上较慢,您是否可以证明虚拟函数执行所需的时间比普通的类方法多?很容易在没有看到某些代码的情况下就无法了解发生的情况。
Answers:
虚拟方法通常是通过所谓的虚拟方法表(简称vtable)实现的,其中存储了函数指针。这将间接添加到实际的调用中(必须从vtable获取要调用的函数的地址,然后再调用它-而不是直接调用它)。当然,这需要一些时间和更多代码。
但是,它不一定是速度缓慢的主要原因。真正的问题是,编译器(通常/通常)无法知道将调用哪个函数。因此,它无法内联它或执行任何其他此类优化。仅此一项可能会添加十几条毫无意义的指令(准备寄存器,调用然后在随后恢复状态),并可能抑制其他看似无关的优化。此外,如果您通过调用许多不同的实现而疯狂地分支,则遭受与遭受疯狂而通过其他方式分支一样的命中:高速缓存和分支预测器将无济于事,分支将花费比完全可预测的时间更长的时间科。
大但是:这些性能影响通常很小而无所谓。如果您要创建高性能代码并考虑添加一个虚假的函数,该函数会以惊人的频率被调用,那么值得考虑。但是,也请记住,与分支的其他方式替换虚函数的调用(if .. else
,switch
,函数指针,等等),解决不了根本问题-它很可能会比较慢。问题(如果根本存在)不是虚函数,而是(不必要的)间接。
编辑:通话说明中的差异在其他答案中有所描述。基本上,静态(“正常”)调用的代码是:
虚拟调用的功能完全相同,只是在编译时不知道函数地址。相反,有一些说明...
至于分支:分支是指跳转到另一条指令而不仅仅是执行下一条指令的任何事物。其中包括if
,,switch
各种循环的一部分,函数调用等,有时编译器实现的事情似乎并不以实际需要分支的方式分支。请参阅为什么处理排序数组比未排序数组快?为什么会变慢,CPU如何应对这种变慢以及这不是万能药。
这是分别从虚拟函数调用和非虚拟调用中得到的一些实际反汇编的代码:
mov -0x8(%rbp),%rax
mov (%rax),%rax
mov (%rax),%rax
callq *%rax
callq 0x4007aa
您可以看到虚拟调用需要另外三个指令来查找正确的地址,而非虚拟调用的地址可以被编译。
但是,请注意,在大多数情况下,额外的查找时间可以忽略不计。在查找时间很长的情况下(例如在循环中),通常可以通过在循环之前执行前三个指令来缓存值。
查找时间变得很重要的另一种情况是,如果您有一个对象集合,并且正在通过循环调用每个对象的虚函数。但是,在这种情况下,您将需要一些方法来选择无论如何都要调用哪个函数,并且虚拟表查找与任何方法一样好。事实上,由于虚函数表查找代码是如此广泛的应用是高度优化的,所以试图解决它具备手动导致一个很好的机会,更糟糕的表现。
-0x8(%rbp)
。哦,我的... AT&T语法。
比什么还慢?
虚函数解决了直接函数调用无法解决的问题。通常,您只能比较两个计算相同内容的程序。“这种光线追踪器比编译器快”是没有道理的,并且该原理甚至可以推广到诸如单个函数或编程语言构造之类的小事情上。
如果您不使用虚函数根据基准来动态切换到一段代码,例如对象的类型,那么您将不得不使用其他内容(例如switch
语句)来完成相同的事情。别的东西都有自己的开销,加上对程序组织的影响,这会影响程序的可维护性和全局性能。
请注意,在C ++中,对虚函数的调用并不总是动态的。当对确切类型已知的对象进行调用时(由于该对象不是指针或引用,或者因为可以静态推断其类型),这些调用只是常规成员函数调用。这不仅意味着没有调度开销,而且这些调用可以以与普通调用相同的方式内联。
换句话说,当虚拟函数不需要虚拟调度时,您的C ++编译器就可以工作,因此通常没有理由担心它们相对于非虚拟函数的性能。
新增内容:此外,我们一定不能忘记共享库。如果您使用的是共享库中的类,则对普通成员函数的调用将不会像那样简单地成为一个很好的指令序列callq 0x4007aa
。它必须经历一些麻烦,例如通过“程序链接表”或某些此类结构进行间接访问。因此,共享库间接调用可以在某种程度上(如果不是完全的话)平衡(真正间接)虚拟调用和直接调用之间的成本差异。因此,在进行虚函数折衷的推理时必须考虑程序的构建方式:目标对象的类是否整体链接到进行调用的程序中。