为什么虚函数调用比dynamic_cast更快?

13

我写了一个简单的例子,使用基类接口和 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的时间要多出两倍。

callDynamiccast的时间:0.000240333

callVirtual的时间:0.0001401

有什么想法原因是什么吗?

编辑:现在在单独的函数中创建对象,所以编译器不知道其真实类型。结果几乎相同。

编辑2:创建两个不同类型的派生对象。


1
你可能需要运行更多次迭代才能获得合适的统计指标。你是否以最高优化设置进行编译? - Oliver Charlesworth
4
你的测试无效,因为编译器可以轻易地将虚函数调用(转换为非虚函数调用)和dynamic_cast(转换为空操作),因为它知道“ptr”实际上指向一个“Derived”对象。 - Johannes Schaub - litb
3
编写一个函数 Base* createBase(),该函数随机返回 Base*Derived*,并在每个循环迭代中调用它。 - ipc
2
标题说dynamic_cast更快,但数字显示virtual更快。我认为这是可以预料的。 - user180326
3
如果你关闭了优化功能,那么测试的结果就毫无意义。 - Oliver Charlesworth
显示剩余6条评论
4个回答

21

虚函数调用类似于函数指针,或者如果编译器知道类型,则为静态分派。这是常数时间。

dynamic_cast有很大的不同--它使用实现定义的方法确定类型。它不是常数时间,可能遍历类层次结构(还要考虑多继承)并执行几个查找。一个实现可以使用字符串比较。因此,在两个维度上复杂度更高。实时系统通常基于这些原因避免/不鼓励使用dynamic_cast

有关详细信息,请参见此文档


那么,如果我扩展类层次结构,dynamic_cast 的时间会更长吗? - D_E
4
@Dmitry,如何实现由你的实现定义,但一般来说是正确的。继承的复杂度(基类数量以及是否使用多重继承)通常是引入成本的地方。如果类之间没有关联,你的实现可能对该情况进行了良好的优化。还要注意一些边缘情况会增加复杂性,其中“dynamic_cast”可能会失败,因为无法确定单个基类,因为存在两个公共基类。因此,在返回之前必须检查整个层次结构。 - justin
链接文档的第31页详细介绍了虚拟调用与多个编译器的各种dynamic_cast之间的区别(尽管我找不到它告诉你使用了哪些编译器)。 - oz10

10

需要注意的是,虚函数的整个目的就是为了不必向下转换继承图。虚函数的存在使得您可以将派生类实例用作基类,以便可以从最初调用基类版本的代码中调用更加专业化的函数实现。

如果虚函数比安全地向派生类转换再调用更慢,那么C++编译器会直接使用后者来实现虚函数调用。

因此,没有理由指望dynamic_cast+调用比虚函数调用更快。


5
你只是在衡量 dynamic_cast<> 的成本。它是使用 RTTI 实现的,这是任何 C++ 编译器中可选的。 项目 + 属性,C/C++,语言,启用运行时类型信息设置。 将其更改为“否”。
现在,您将收到一个不太明显的提醒,即 dynamic_cast<> 无法执行适当的工作。 任意更改为 static_cast<> 将获得截然不同的结果。 关键点在于,如果你“知道”向上转换总是安全的,那么 static_cast<> 可以帮助你获取所需的性能。 如果你不能确定向上转换是否安全,则 dynamic_cast<> 可以让你免于麻烦。 这是一种极难诊断的麻烦。 常见的故障模式是堆栈损坏,如果你很幸运,它只会触发立即 GPF。

3
区别在于,您可以在任何派生自Base的实例上调用虚函数。成员函数notVirtualCall()不存在于Base中,并且在确定对象的确切动态类型之前无法调用。
这种差异的后果是,基类的vtable包括一个virtualCall()插槽,其中包含指向正确函数的函数指针。因此,虚调用只需追踪所有Base类型对象的第一个(不可见)成员作为vtable指针,从对应于virtualCall()的插槽中加载指针,并调用该指针后面的函数。
相比之下,当您执行dynamic_cast<>时,类Base在编译时不知道其他类最终会从它派生出来。因此,它不能在其vtable中包含有助于解决dynamic_cast<>的信息。这就是信息缺失,使得dynamic_cast<>的实现比虚函数调用更加昂贵。dynamic_cast<>必须实际搜索实际对象的继承树,以检查转换的目标类型是否在其基础中找到。这是虚调用避免的工作。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接