我写了一个简单的例子,它使用基类接口和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
任何想法为什么会这样?
EDITED:对象创建现在是用separete函数创建的,所以编译器不知道它是真正的类型。几乎相同的结果。
EDITED2:创建两种不同类型的派生对象。
答案 0 :(得分:12)
虚函数调用类似于函数指针,或者如果编译器知道类型,静态调度。这是不变的时间。
dynamic_cast
完全不同 - 它使用实现定义的方法来确定类型。它不是常量时间,可以遍历类层次结构(也考虑多重继承)并执行多次查找。实现可以使用字符串比较。因此,复杂性在两个维度上更高。由于这些原因,实时系统通常会避免/阻止dynamic_cast
。
有更多详情可供in this document。
答案 1 :(得分:4)
应该注意的是,虚函数的整个目的不必抛弃继承图。存在虚函数,以便您可以使用派生类实例,就像它是基类一样。因此,可以从最初称为基类版本的代码中调用更专业的函数实现。
如果虚函数比派生类+函数调用的安全转换慢,那么C ++编译器就会以这种方式实现虚函数调用。
所以没有理由期望dynamic_cast
+电话更快。
答案 2 :(得分:3)
您只是衡量dynamic_cast<>
的费用。它是用RTTI实现的,在任何C ++编译器中都是可选的。项目+属性,C / C ++,语言,启用运行时类型信息设置。将其更改为否。
现在,您将获得一个不明确的提醒:dynamic_cast<>
无法再正常工作了。任意将其更改为static_cast<>
以获得截然不同的结果。这里的关键点是,如果您知道上传始终是安全的,那么static_cast<>
会为您购买您正在寻找的性能。如果您不知道有关上传是安全的事实,那么dynamic_cast<>
会让您免于麻烦。这是一种难以诊断的麻烦。常见的故障模式是堆损坏,如果你真的很幸运,你只能得到一个直接的GPF。
答案 3 :(得分:0)
不同之处在于,您可以在从Base
派生的任何实例上调用虚函数。 notVirtualCall()
成员在Base
中不存在,如果没有先确定对象的确切动态类型,则无法调用。
这种差异的结果是,基类的vtable包含virtualCall()
的一个槽,它包含一个指向要调用的正确函数的函数指针。因此,虚拟调用只是追逐包含为Base
类型的所有对象的第一个(不可见)成员的vtable指针,从对应于virtualCall()
的槽中加载指针,并调用该指针后面的函数
相比之下,当您执行dynamic_cast<>
时,类Base
在编译时不知道其他类最终会从中获取什么。因此,它不能在其vtable中包含简化dynamic_cast<>
的解决方案的信息。这就是缺少使得dynamic_cast<>
实现比虚拟函数调用更昂贵的信息。 dynamic_cast<>
必须实际搜索实际对象的继承树,以检查是否在其基础中找到了转换的目标类型。这是虚拟呼叫避免的工作。