是否可以强制编译器在间接调用派生类时静态解释虚函数,以避免vtable-cost?为什么呢?
我创建了一个测试来研究final
- 关键字对vtable成本的影响。
B
派生自班级A
。A::f1()
是非虚拟功能。A::f2()
是一个虚函数。 B
会覆盖它。A::f3()
是一个虚函数。 B
会覆盖它并将其标记为最终。A::f4()
是一个非虚拟函数。它致电A::f3()
。我描述并注意到函数的成本是(相对): -
B*->f1()
= 160 B*->f2()
= 270:"虚拟"费用很高。B*->f3()
= 160:" final"产量增加!B*->f4()
= 270:为什么不是160? < - 问题 编译器似乎查看B::f4()
并尝试拨打A::f3()
,查看vtable,然后拨打B::f3()
。
我认为编译器 静态 知道B*->f4()
会调用B*->f3()
,因此不应该有v-table成本。
f4()
派生的每个类中共享A
的代码(二进制/汇编)。因此,它是防止"代码膨胀",对吗? f4
位于A
,而不会出现在B
。这是测试。
class A{
public: int f1(){return randomNumber*3;};
public: virtual int f2(){return randomNumber*3;};
public: virtual int f3(){return randomNumber*3;};
public: int f4(){return f3();};
public: int randomNumber=((double) rand() / (RAND_MAX))*10;
};
class B : public A {
public: virtual int f2() {return randomNumber*4;};
public: virtual int f3()final {return randomNumber*4;};
};
int main(){
std::vector<B*> bs;
const int numTest=10000;
for(int n=0;n<numTest;n++){
bs.push_back(new B());
};
int accu=0;
for(int n=0;n<numTest;n++){
accu+=bs[n]->f1(); //warm
};
auto t1= std::chrono::system_clock::now();
for(int n=0;n<numTest;n++){
accu+=bs[n]->f1(); //test 1 : base case, non virtual
};
auto t2= std::chrono::system_clock::now();
for(int n=0;n<numTest;n++){
accu+=bs[n]->f2(); //test 2: virtual
};
auto t3= std::chrono::system_clock::now();
for(int n=0;n<numTest;n++){
accu+=bs[n]->f3(); //test 3: virtual & final
};
auto t4= std::chrono::system_clock::now();
for(int n=0;n<numTest;n++){
accu+=bs[n]->f4(); //test 4: virtual & final & encapsulator
};
auto t5= std::chrono::system_clock::now();
auto t21=t2-t1;
auto t32=t3-t2;
auto t43=t4-t3;
auto t54=t5-t4;
std::cout<<"test1 base ="<<t21.count()<<std::endl;
std::cout<<"test2 virtual ="<<t32.count()<<std::endl;
std::cout<<"test3 virtual & final ="<<t43.count()<<std::endl;
std::cout<<"test4 virtual & final & indirect="<<t54.count()<<std::endl;
std::cout<<"forbid optimize"<<accu;
}
很抱歉,如果我使用错误的术语,我对C ++很新
这个问题来自好奇心
在实践中,可以通过将f4()
移至B
来解决,但我想知道其背后的基本原理。
答案 0 :(得分:1)
问题是您的示例中没有B::f4()
。因此,唯一的f4
是A::f4()
。并且必须使用A中的所有派生类。
正如您所注意到的,您可以编写自己的B::f4()
,然后重载(不会被覆盖)。然后编译器在知道您正在访问B时调用B::f4()
。在B::f4()
中,编译器应该足够聪明以直接使用B::f3()
。
如果通过A引用或指针访问B,编译器将继续使用A::f4()
。
当我在编译器资源管理器上尝试此操作时,只有2017编译器B::f3
在B::f4
内联,并且都按预期进入调用函数。
当我没有定义B::f4
时,A::f4
被内联并仍然执行虚函数调用。
在内联f4之后,您的编译器似乎无法对虚函数调用做出正确的推理。我只能推测Microsoft编译器的工作原理,但gcc和LLVM分别编译为语言无关的中间形式(GIMPLE格式和LLVM IR),并对其进行优化。之后,这将成为一个别名问题,编译器必须静态地证明虚拟表中的条目始终为B::f3
。通常它不能确定,不幸的是,关于最终方法的信息似乎不能传播得足够远。如果GCC看起来有利可图,它至少会speculative devirtualization。
当没有内联发生时,我认为编译器将很难优化它,即使它一次看到所有定义也无法保证。
提供额外的&#34;专业化&#34;对于类型B的对象,A::f4
在理论上是可行的,但我不确定它是否足以让编译器开发人员认为有足够的平均案例性能。
实现f4的一种方法是让编译器生成你想要的代码变体而不必重复自己将作为A外部的模板函数:
template <typename DerivedFromA>
inline int f4(DerivedFromA &x)
{
return x.f3();
}