为啥虚函数调用比dynamic_cast快?

Posted

技术标签:

【中文标题】为啥虚函数调用比dynamic_cast快?【英文标题】:Why virtual function call is faster than dynamic_cast?为什么虚函数调用比dynamic_cast快? 【发布时间】:2012-03-31 20:18:10 【问题描述】:

我写了一个简单的例子,估计调用虚函数的平均时间,使用基类接口和dynamic_cast以及调用非虚函数。 就是这样:

#include <iostream>
#include <numeric>
#include <list>
#include <time.h>

#define CALL_COUNTER (3000)

__forceinline int someFunction()

  return 5;


struct Base

  virtual int virtualCall() = 0;
  virtual ~Base();
;

struct Derived : public Base

  Derived();
  virtual ~Derived();
  virtual int virtualCall() return someFunction(); ;
  int notVirtualCall() return someFunction(); ;
;


struct Derived2 : public Base

  Derived2();
  virtual ~Derived2();
  virtual int virtualCall() return someFunction(); ;
  int notVirtualCall() return someFunction(); ;
;

typedef std::list<double> Timings;

Base* createObject(int i)

  if(i % 2 > 0)
    return new Derived(); 
  else 
    return new Derived2(); 


void callDynamiccast(Timings& stat)

  for(unsigned i = 0; i < CALL_COUNTER; ++i)
  
    Base* ptr = createObject(i);

    clock_t startTime = clock();

    for(int j = 0; j < CALL_COUNTER; ++j)
    
      Derived* x = (dynamic_cast<Derived*>(ptr));
      if(x) x->notVirtualCall();
    

    clock_t endTime = clock();
    double callTime = (double)(endTime - startTime) / CLOCKS_PER_SEC;
    stat.push_back(callTime);

    delete ptr;
  


void callVirtual(Timings& stat)

  for(unsigned i = 0; i < CALL_COUNTER; ++i)
  
    Base* ptr = createObject(i);

    clock_t startTime = clock();

    for(int j = 0; j < CALL_COUNTER; ++j)
      ptr->virtualCall();


    clock_t endTime = clock();
    double callTime = (double)(endTime - startTime) / CLOCKS_PER_SEC;
    stat.push_back(callTime);

     delete ptr;
  


int main()

  double averageTime = 0;
  Timings timings;


  timings.clear();
  callDynamiccast(timings);
  averageTime = (double) std::accumulate<Timings::iterator, double>(timings.begin(), timings.end(), 0);
  averageTime /= timings.size();
  std::cout << "time for callDynamiccast: " << averageTime << std::endl;

  timings.clear();
  callVirtual(timings);
  averageTime = (double) std::accumulate<Timings::iterator, double>(timings.begin(), timings.end(), 0);
  averageTime /= timings.size();
  std::cout << "time for callVirtual: " << averageTime << std::endl;

  return 0;

看起来 callDynamiccast 需要将近两倍的时间。

time for callDynamiccast: 0.000240333

time for callVirtual: 0.0001401

有什么想法吗?

已编辑:对象创建现在是在单独的函数中进行的,因此编译器不知道它的真实类型。结果几乎一样。

EDITED2:创建两种不同类型的派生对象。

【问题讨论】:

您可能需要运行更多的迭代才能获得体面的统计测量。您是否在最高优化设置下进行编译? 您的测试无效,因为编译器可以轻松优化虚拟调用(变成非虚拟调用)和dynamic_cast(基本上变成noop),因为它知道ptr 确实指向一个Derived 对象。 编写一个函数Base* createBase()随机返回Base*Derived*,并在每次循环迭代中调用它。 嗯,标题说 dynamic_cast 更快,但数字显示 virtual 更快。我认为这是意料之中的。 @DmitryEskin:如果您关闭了优化,那么测试结果绝对没有任何意义...... 【参考方案1】:

虚函数调用类似于函数指针,或者如果编译器知道类型,则静态调度。这是恒定的时间。

dynamic_cast 完全不同——它使用实现定义的方法来确定类型。它不是恒定的时间,可能会遍历类层次结构(也可以考虑多重继承)并执行多次查找。实现可以使用字符串比较。因此,二维的复杂度更高。由于这些原因,实时系统通常会避免/阻止dynamic_cast

更多详情请见in this document。

【讨论】:

那么,如果我扩展类层次结构,dynamic_cast 时间会增加更多吗? @Dmitry 它的实现方式由您的实现定义,但作为概括,是的,这是正确的。继承的复杂性(基数以及是否使用多重继承)通常是引入成本的地方。如果类不相关,您的实现可能对这种情况有很好的优化。另请注意,有一些边缘情况会增加复杂性——dynamic_cast 可能会失败,因为存在两个公共基础,因此无法确定单个基础。因此,在返回之前必须检查整个层次结构。 链接文档的第 31 页包含有关虚拟调用与多个编译器的各种 dynamic_cast 的相关详细信息(尽管我找不到它告诉您使用了哪些编译器的位置)。【参考方案2】:

应该注意的是,虚函数的全部目的是不必放弃继承图。存在虚函数,因此您可以像使用基类一样使用派生类实例。这样可以从最初调用基类版本的代码中调用更专业的函数实现。

如果虚函数比安全强制转换为派生类 + 函数调用慢,那么 C++ 编译器将简单地以这种方式实现虚函数调用。

所以没有理由期望dynamic_cast+call 会更快。

【讨论】:

【参考方案3】:

您只是在衡量dynamic_cast&lt;&gt; 的成本。它是用 RTTI 实现的,这在任何 C++ 编译器中都是可选的。项目 + 属性、C/C++、语言、启用运行时类型信息设置。改为否。

您现在会得到一个明确的提醒,dynamic_cast&lt;&gt; 不能再做正确的工作。随意将其更改为static_cast&lt;&gt; 以获得截然不同的结果。这里的关键点是,如果您知道向上转换总是安全的,那么static_cast&lt;&gt; 会为您购买所需的性能。如果您不知道 upcast 是安全的,那么dynamic_cast&lt;&gt; 可以让您远离麻烦。这是一种非常难以诊断的问题。常见的故障模式是堆损坏,如果你真的很幸运,你只能立即获得 GPF。

【讨论】:

【参考方案4】:

不同之处在于,您可以在派生自Base 的任何实例上调用虚函数。 notVirtualCall() 成员在 Base 中不存在,并且在未先确定对象的确切动态类型之前无法调用。

这种差异的结果是,基类的 vtable 包含一个用于virtualCall() 的槽,其中包含一个指向要调用的正确函数的函数指针。因此,虚拟调用只是简单地追踪作为所有 Base 类型对象的第一个(不可见)成员的 vtable 指针,从对应于 virtualCall() 的槽中加载指针,并调用该指针后面的函数。

当您执行dynamic_cast&lt;&gt; 时,相比之下,Base 类在编译时不知道其他类最终会从它派生出什么。因此,它不能在其 vtable 中包含有助于解决dynamic_cast&lt;&gt; 的信息。这就是信息的缺失导致dynamic_cast&lt;&gt; 的实现比虚函数调用更昂贵。 dynamic_cast&lt;&gt; 必须实际搜索实际对象的继承树,以检查是否在其基础中找到了转换的目标类型。这是虚拟调用避免的工作。

【讨论】:

以上是关于为啥虚函数调用比dynamic_cast快?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 Python 的 'len' 函数比 __len__ 方法快?

将基类作为参数传递给虚函数时避免使用 dynamic_cast

C++子类如何调父类的虚函数

c++ 虚函数 如何调父类 而非子类

为啥使用存储在虚方法表中的地址对虚函数的函数调用返回垃圾?

为何需要虚函数 直接对象名加作用域来调用子类父类爷类的函数不就行了 为啥