为啥虚函数调用比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<>
的成本。它是用 RTTI 实现的,这在任何 C++ 编译器中都是可选的。项目 + 属性、C/C++、语言、启用运行时类型信息设置。改为否。
您现在会得到一个明确的提醒,dynamic_cast<>
不能再做正确的工作。随意将其更改为static_cast<>
以获得截然不同的结果。这里的关键点是,如果您知道向上转换总是安全的,那么static_cast<>
会为您购买所需的性能。如果您不知道 upcast 是安全的,那么dynamic_cast<>
可以让您远离麻烦。这是一种非常难以诊断的问题。常见的故障模式是堆损坏,如果你真的很幸运,你只能立即获得 GPF。
【讨论】:
【参考方案4】:不同之处在于,您可以在派生自Base
的任何实例上调用虚函数。 notVirtualCall()
成员在 Base
中不存在,并且在未先确定对象的确切动态类型之前无法调用。
这种差异的结果是,基类的 vtable 包含一个用于virtualCall()
的槽,其中包含一个指向要调用的正确函数的函数指针。因此,虚拟调用只是简单地追踪作为所有 Base
类型对象的第一个(不可见)成员的 vtable 指针,从对应于 virtualCall()
的槽中加载指针,并调用该指针后面的函数。
当您执行dynamic_cast<>
时,相比之下,Base
类在编译时不知道其他类最终会从它派生出什么。因此,它不能在其 vtable 中包含有助于解决dynamic_cast<>
的信息。这就是信息的缺失导致dynamic_cast<>
的实现比虚函数调用更昂贵。 dynamic_cast<>
必须实际搜索实际对象的继承树,以检查是否在其基础中找到了转换的目标类型。这是虚拟调用避免的工作。
【讨论】:
以上是关于为啥虚函数调用比dynamic_cast快?的主要内容,如果未能解决你的问题,请参考以下文章
为啥 Python 的 'len' 函数比 __len__ 方法快?