12

我写了一个简单的例子,估计调用虚函数的平均时间,使用基类接口和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:创建两种不同类型的派生对象。

4

4 回答 4

19

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

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

本文档提供了更多详细信息。

于 2012-03-31T20:58:54.897 回答
10

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

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

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

于 2012-03-31T21:31:18.093 回答
5

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

您现在会收到一个不明显的提醒,提示dynamic_cast<>您无法再做正确的工作。任意更改它以static_cast<>获得截然不同的结果。这里的关键点是,如果您知道向上转换总是安全的,那么static_cast<>就会为您购买您正在寻找的性能。如果您不知道upcast 是安全的,那么dynamic_cast<>您就不会遇到麻烦。这是一种非常难以诊断的问题。常见的故障模式是堆损坏,如果您真的很幸运,您只能立即获得 GPF。

于 2012-03-31T21:25:52.107 回答
2

不同之处在于,您可以在任何派生自Base. 该notVirtualCall()成员不存在于 中Base,并且在没有首先确定对象的确切动态类型之前无法调用。

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

dynamic_cast<>相反,当您执行 a时,该类Base在编译时不知道其他类最终会从它派生什么。因此,它不能在其 vtable 中包含有助于解决dynamic_cast<>. 那是信息的缺乏使得dynamic_cast<>实现比虚函数调用更昂贵。必须实际搜索实际对象的dynamic_cast<>继承树,以检查转换的目标类型是否在其基础中找到。这是虚拟呼叫避免的工作。

于 2017-04-04T13:28:16.333 回答