我试图了解下面的哪个实现“更快”。假设使用和不使用-DVIRTUAL标志编译此代码。
我认为没有-DVIRTUAL的编译会更快,因为:
a]没有使用vtable
b]编译器可能能够优化汇编指令,因为它“确切地”知道在给定各种选项的情况下将进行哪个调用(只有有限数量的选项)。
我的问题是与速度有关,而不是漂亮的代码。
a]我在上面的分析中是否正确?
b]分支预测器/编译器组合是否足够智能以优化switch语句的给定分支?看到“type”是一个const int。
c]我还缺少其他因素吗?
谢谢!
#include <iostream>
class Base
{
public:
Base(int t) : type(t) {}
~Base() {}
const int type;
#ifdef VIRTUAL
virtual void fn1()=0;
#else
void fn2();
#endif
};
class Derived1 : public Base
{
public:
Derived1() : Base(1) { }
~Derived1() {}
void fn1() { std::cout << "in Derived1()" << std::endl; }
};
class Derived2 : public Base
{
public:
Derived2() : Base(2) { }
~Derived2() { }
void fn1() { std::cout << "in Derived2()" << std::endl; }
};
#ifndef VIRTUAL
void Base::fn2()
{
switch(type)
{
case 1:
(static_cast<Derived1* const>(this))->fn1();
break;
case 2:
(static_cast<Derived2* const>(this))->fn1();
break;
default:
break;
};
}
#endif
int main()
{
Base *test = new Derived1();
#ifdef VIRTUAL
test->fn1();
#else
test->fn2();
#endif
return 0;
}
答案 0 :(得分:1)
我认为你误解了VTable。 VTable只是一个跳转表(在大多数实现中,尽管AFAIK规范并不能保证这一点!)。事实上,你可以说它是一个巨大的转换声明。因此,我打赌速度与你的两种方法完全相同。
如果有什么我认为VTable方法会稍微更快,因为编译器可以做出更好的决策来优化缓存对齐等等......
答案 1 :(得分:1)
您是否测量过表现以确定是否存在任何差异?
我想不是,因为那时你不会在这里问。这是唯一合理的回应。
答案 2 :(得分:1)
如果不指定编译器和编译器选项,则无法回答。
我认为没有什么特别的理由说明为什么非虚拟代码必须比虚拟代码更快地进行调用。事实上,交换机可能比vtable慢,因为使用vtable的调用将加载一个地址并跳转到它,而交换机将加载一个整数并做一些思考。他们中的任何一个都可以更快。由于显而易见的原因,标准未指定虚拟调用“比您发明的替换它的任何其他东西慢”。
我认为随机选择的编译器实际上不太可能在虚拟情况下内联调用,但肯定允许(在as-if规则下),因为*test
的动态类型可能是通过数据流分析或类似方法确定。我认为,通过优化启用,随机选择的编译器可以内联非虚拟情况下的所有内容。但是,你在一个TU中给出了一个功能非常短的小例子,所以内联特别容易。
答案 3 :(得分:1)
假设您没有过早地进行微观优化,并且您已经对代码进行了分析并发现这是一个需要解决的问题,那么找出问题答案的最佳方法是在发布时完整地编译优化并检查生成的机器代码。
答案 4 :(得分:0)
避免vtables的速度并不一定是更快 - 确定,你应该衡量自己。
答案 5 :(得分:0)
请注意:
static_cast
版本可能会引入一个分支(如果它被优化为跳转表,可能不会),vtable
版本会产生跳转表。在这看一个模式?
通常,您更喜欢线性时间查找,而不是分支代码,因此虚函数方法似乎更好。
答案 6 :(得分:0)
这取决于平台和编译器。 switch
语句可以实现为测试和分支或跳转表(即,间接分支)。 virtual
函数通常实现为间接分支。如果编译器将switch
语句转换为跳转表,则这两种方法会有一个额外的解引用。如果是这种情况并且这种特殊用法不经常发生(或者使缓存充分崩溃),那么由于额外的缓存未命中,您可能会看到差异。
另一方面,如果switch
语句只是一个测试和分支,您可能会发现某些有序CPU在间接分支上刷新指令缓存会产生更大的性能差异(或者需要一个设置间接分支的目标和跳转到它之间的高延迟)。
如果你真的关心虚函数调度的开销,比如,对于异构对象集合的内循环,你可能想重新考虑执行动态调度的位置。它不一定是每个对象;它也可以是每个已知的具有相同类型的对象分组。