虚函数实现

时间:2010-03-09 01:29:47

标签: c++ virtual

我一直听到这个说法。 Switch ..Case是代码维护的邪恶,但它提供了更好的性能(因为编译器可以内联东西等)。虚函数非常适合代码维护,但它们会导致两个指针间接的性能损失。

假设我有一个带有2个子类(X和Y)和一个虚函数的基类,因此会有两个虚拟表。该对象有一个指针,根据该指针选择一个虚拟表。所以对于编译器来说,它更像是

switch( object's function ptr )
{

   case 0x....:

       X->call();

       break;

   case 0x....:

       Y->call();
};

那么为什么虚拟函数会花费更多,如果它可以通过这种方式实现,因为编译器可以在这里执行相同的内联和其他内容。或者解释一下,为什么决定不以这种方式实现虚函数执行?

谢谢, 戈库尔。

8 个答案:

答案 0 :(得分:2)

由于单独的编译模型,编译器无法做到这一点。

在编译虚函数调用时,编译器无法确定有多少不同的子类。

考虑以下代码:

// base.h
class base
{
public:
    virtual void doit();
};

和此:

// usebase.cpp
#include "base.h"

void foo(base &b)
{
    b.doit();
}

当编译器在foo中生成虚拟调用时,它不知道运行时将存在哪个base的子类。

答案 1 :(得分:1)

您的问题取决于对交换机和虚拟功能的工作方式的误解。我不会用代码生成的长篇论文来填充这个框,而是给出一些要点:

  • 切换语句不一定比虚函数调用或内联更快。您可以详细了解将switch语句转换为程序集herehere的方式。
  • 关于虚函数调用的速度慢的东西不是指针查找,而是间接分支。对于complicated reasons having to do with the internal electronics of the CPU,对于大多数现代处理器,执行“直接分支”(其中目标地址在指令中编码)比在运行时计算地址的“indirect branch”更快。虚函数调用和大型switch语句通常实现为间接分支。
  • 在上面的示例中,交换机完全冗余。一旦计算了对象的成员函数指针,CPU就可以直接分支到它。即使链接器知道可执行文件中存在的每个可能的成员对象,仍然没有必要添加该表查找。

答案 2 :(得分:1)

以下是具体测试的一些结果。这些特定结果来自VC ++ 9.0 / x64:

Test Description: Time to test a global using a 10-way if/else if statement
CPU Time:        7.70  nanoseconds           plus or minus      0.385

Test Description: Time to test a global using a 10-way switch statement
CPU Time:        2.00  nanoseconds           plus or minus     0.0999

Test Description: Time to test a global using a 10-way sparse switch statement
CPU Time:        3.41  nanoseconds           plus or minus      0.171

Test Description: Time to test a global using a 10-way virtual function class
CPU Time:        2.20  nanoseconds           plus or minus      0.110

对于稀疏情况,switch语句要慢得多。对于密集的情况,switch语句可能会更快,但是开关和虚函数调度重叠一点,所以当开关可能更快时,边距太小我们甚至无法确定它更快,更不用说足够快得多关心了。如果switch语句中的情况完全是稀疏的,那么虚函数调用的速度就会越来越快。

答案 3 :(得分:0)

虚拟调度中没有分支。类中的vptr指向一个vtable,其中第二个指针用于常量偏移处的特定函数。

答案 4 :(得分:0)

关于调用虚函数时分支的说法是错误的。生成的代码中没有这样的东西。看看汇编代码会给你一个更好的主意。

在一个坚果shell中,C ++虚函数的一个通用简化实现是:每个类都有一个虚拟表(vbtl),并且该类的每个实例都有一个虚拟表指针(vptr)。虚拟表基本上是一个函数指针列表。

当您调用虚拟函数时,请说它是:

class Base {};
class Derived {};
Base* pB = new Derived();
pB->someVirtualFunction();

'someVirtualFunction()'将在vtbl中具有相应的索引。和电话

pB->someVirtualFunction(); 

将转换为类似:

pB->vptr[k](); //k is the index of the 'someVirtualFunction'.

通过这种方式,函数实际上是间接调用的,并且具有多态性。

我建议你阅读Stanley Lippman撰写的'The C++ Object Model'

此外,虚函数调用比switch-case慢的语句不会产生。这取决于。正如您在上面所看到的,与常规函数调用相比,虚函数调用只需要一次额外的引用时间。使用switch-case分支,你会有额外的比较逻辑(这会导致CPU丢失缓存的可能性),这也消耗CPU周期。我会说在大多数情况下,如果不是全部,虚拟函数调用应该比switch-case更快。

答案 5 :(得分:0)

实际上,如果你有很多虚函数,那么类似switch的分支将比两个指针间接更慢。当前实现的性能并不取决于您拥有多少虚拟功能。

答案 6 :(得分:0)

明确地说switch/case比虚拟调用更多或更少的性能是一种过度概括。事实是,这将取决于许多事情,并将根据以下内容而有所不同:

  • 您正在使用的编译器
  • 启用了哪些优化
  • 您的计划的整体特征及其对这些优化的影响

如果您在编写代码时正在优化代码,那么您很可能做出错误的选择。首先以人类可读和/或用户友好的方式编写代码,然后通过分析工具运行整个可执行文件。如果代码的这个区域显示为热点,那么请尝试两种方式,看看哪种方式可以量化为您的特定情况。

答案 7 :(得分:0)

这种优化只能通过重新定位链接器实现,该链接器应作为C ++运行时的一部分运行。

C ++运行时更复杂,即使新的DLL加载(使用COM)也会向vtable添加新的函数指针。 (想想纯粹的虚拟fns?)

然后编译器或链接器都无法进行此优化。 switch / case明显比间接调用快,因为CPU中的预取是确定性的并且可以进行流水线操作。但是由于对象的vtable的运行时扩展,它在C ++中无法解决。