【问题标题】:Derive* call non-virtual Base's function THAT call virtual function - force statically resolved派生*调用非虚拟基的函数调用虚拟函数 - 强制静态解析
【发布时间】:2017-08-08 10:01:45
【问题描述】:

当间接调用派生类中的虚函数时,是否可以强制编译器对其进行静态解释,以避免 vtable-cost?为什么?

示例

我创建了一个测试来研究final-keyword 对 vtable 成本的影响。

  • B派生自类A
  • A::f1() 是一个虚函数。
  • A::f2() 是一个虚函数。 B 覆盖它。
  • A::f3() 是一个虚函数。 B 覆盖它并将其标记为 final
  • A::f4() 是一个虚函数。它调用A::f3()

Full Demo

我分析并注意到功能的成本是(相对):-

  • B*->f1() = 160
  • B*->f2() = 270 :“虚拟”成本很高。
  • B*->f3() = 160:“最终”收益性能提升!
  • B*->f4() = 270:为什么不是 160?

编译器似乎在查看B::f4(),并尝试调用A::f3(),查看vtable,然后调用B::f3()

我相信编译器应该静态地知道B*->f4()会调用B*->f3(),所以应该没有v表开销。

问题

  • 为什么它不知道?
  • 我相信如果它被静态解析,编译器将不可能在派生自A 的每个类之间共享f4() 的(二进制/汇编)代码。因此,这是为了防止“代码膨胀”,对吗?
  • 如果是这样,我该如何强制“代码膨胀”?我仍然希望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来解决,但我想知道它背后的原理。

【问题讨论】:

  • 您应该尝试将B 声明为最终结果。也许编译器会尝试去虚拟化一些调用。但总的来说你不应该过度依赖编译器。
  • @VTT 无济于事,f4() 变得更加昂贵。 rextester.com/CIG54180
  • 你使用什么编译器和编译器标志?
  • @mars 根据 rextester,我正在使用 Visual C++ source_file.cpp -O2 -o a.exe /EHsc /MD /I C:\boost_1_60_0 /link /LIBPATH:C:\boost_1_60_0\stage\lib
  • 与非最终版本相比,test2 的结果确实有所改善。

标签: c++ optimization c++14 final vtable


【解决方案1】:

问题是您的示例中没有B::f4()。所以唯一的f4A::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

当没有内联发生时,我认为编译器将很难优化这一点,即使它一次看到所有定义也不能保证。 为 B 类型的对象提供一个额外的A::f4 的“特化”在理论上是可行的,但我不确定它是否能提供足够的平均案例性能以让编译器开发人员认为值得。

实现 f4 以使编译器生成您想要的代码变体而您不必重复自己的一种方法是作为 A 外部的模板函数:

template <typename DerivedFromA>
inline int f4(DerivedFromA &x)
{
  return x.f3();
}

【讨论】:

  • 谢谢。 1. The latest GCC and clang seem to be able to do that 你的意思是demo中的f4() 在gcc中应该快吗?证据rextester.com/WZET4221(使用时间,gcc)表明它不能内联。这与你的说法相矛盾。不过,我不确定 rextester 是 2017 年。您是否在您的计算机或在线网站上对其进行了测试? 2. gcc and LLVM compile to a language agnostic intermediate form :您可以提供参考吗?这对我来说是新知识。
  • 1.我在 gcc.godbolt.com 上尝试了一个简化的示例。然而,我犯了一个错误,因为使用 B 的局部变量。然后编译器比使用 B 指针时知道的更多。请参阅此示例:godbolt.org/g/MaB1Qi。在那里,只执行推测性的去虚拟化。我会纠正的。 2.我添加了相应格式说明的链接。我相信通过谷歌搜索你应该会找到更好的介绍。
猜你喜欢
  • 2020-07-02
  • 2015-02-24
  • 2014-01-04
  • 2018-05-11
  • 1970-01-01
  • 2015-12-15
  • 2014-02-08
  • 2011-11-16
  • 2014-10-02
相关资源
最近更新 更多