C++ 虚拟函数实现

C++ 虚拟函数实现,c++,virtual,C++,Virtual,我一直听到这种说法。Case对于代码维护是有害的,但它提供了更好的性能(因为编译器可以内联文件等)。虚拟函数非常适合代码维护,但它们会导致两个指针间接寻址的性能损失 假设我有一个基类,它有两个子类(X和Y)和一个虚函数,所以会有两个虚表。对象有一个指针,它将根据该指针选择一个虚拟表。所以对于编译器来说,它更像 switch( object's function ptr ) { case 0x....: X->call(); break; ca

我一直听到这种说法。Case对于代码维护是有害的,但它提供了更好的性能(因为编译器可以内联文件等)。虚拟函数非常适合代码维护,但它们会导致两个指针间接寻址的性能损失

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

switch( object's function ptr )
{

   case 0x....:

       X->call();

       break;

   case 0x....:

       Y->call();
};
那么,如果虚拟函数能够以这种方式实现,那么为什么虚拟函数的成本会更高呢?因为编译器在衬里和其他方面也可以做到这一点。或者解释一下,为什么决定不以这种方式实现虚拟函数执行

谢谢,
Gokul.

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

编译器不能这样做,因为有单独的编译模型

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

考虑以下代码:

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

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

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

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

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

在果壳中,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'.
通过这种方式,函数实际上是间接调用的,并且它具有多态性

我建议你读斯坦利·利普曼的


此外,虚拟函数调用比开关情况慢的语句也不会累加。视情况而定。正如您在上面所看到的,与常规函数调用相比,虚拟函数调用的解引用时间只多了1次。使用开关大小写分支,您会有额外的比较逻辑(这会导致CPU缺少缓存),这也会消耗CPU周期。我想说,在大多数情况下(如果不是全部的话),虚拟函数调用应该比开关调用快。

事实上,如果有许多虚拟函数,类似开关的分支将比双指针间接寻址慢。当前实现的性能并不取决于您拥有多少虚拟函数。

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

  • 您使用的编译器是什么
  • 启用了哪些优化
  • 程序的总体特征以及它们如何影响这些优化

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

您的问题在于对交换机和虚拟函数工作方式的误解。与其用一篇关于代码生成的长篇论文来填满这个盒子,我将给出几个要点:

  • Switch语句不一定比虚拟函数调用或内联调用快。您可以进一步了解将switch语句转换为汇编语句和汇编语句的方式
  • 虚拟函数调用的慢不是指针查找,而是间接分支。例如,对于大多数现代处理器来说,执行“直接分支”(目标地址在指令中编码)比执行“”更快,后者在运行时计算地址。虚拟函数调用和大型开关语句通常作为间接分支实现
  • 在上面的示例中,交换机是完全冗余的。一旦计算了对象的成员函数指针,CPU就可以直接分支到它。即使链接器知道可执行文件中存在的每个可能的成员对象,也不必添加该表查找

以下是一些具体测试的结果。这些特定结果来自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和虚拟函数分派有点重叠,因此虽然switch可能更快,但余量太小了,我们甚至无法确定它是否更快,更不用说它的速度是否足够快了。如果Switter语句中的情况是稀疏的,则没有真正的问题,即虚函数调用会更快。

< P>只有通过重新修补链接器才能实现这些优化,这是C++运行时的一部分。p> <> P> C++运行时更复杂,甚至新的DLL加载(带有COM)将向VTABLE添加新的函数指针。(想想纯粹的虚拟FN?)

那么编译器或链接器都无法进行此优化。switch/case显然比间接调用快,因为CPU中的预取是确定的,并且可以进行流水线。但是,由于对象的VTABLE的这个运行时扩展,C++将无法工作。p> 从技术上讲,这是一个分支;汇编编写器将跳转到存储在寄存器中的地址称为“indirec”