C++ 派生*调用非虚拟基';调用虚函数的s函数-静态解析的力

C++ 派生*调用非虚拟基';调用虚函数的s函数-静态解析的力,c++,optimization,c++14,final,vtable,C++,Optimization,C++14,Final,Vtable,在间接调用派生类中的虚函数时,是否可能强制编译器对其进行静态解释,以避免vtable开销?为什么? 例子 我创建了一个测试来研究final-关键字对vtable成本的影响 类B派生自类A A::f1()是一个非虚拟函数 A::f2()是一个虚拟函数B覆盖它 A::f3()是一个虚拟函数B覆盖它并将其标记为最终 A::f4()是一个非虚拟函数。它调用A::f3() 我概述并注意到功能的成本(相对而言):- B*->f1()=160 B*->f2()=270:“虚拟”成本很高 B*->f3()

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

例子 我创建了一个测试来研究
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:“最终”收益率性能增益
  • B*->f4()
    =270:为什么不是160f4()将调用
    B*->f3()
    ,因此应该没有v-table成本

    问题:
    • 为什么它不知道
    • 我相信如果它是静态解析的,编译器将不可能在从
      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;
    }
    
    A类{
    public:int f1(){返回随机数*3;};
    public:virtualint f2(){return randomNumber*3;};
    public:virtualint f3(){return randomNumber*3;};
    public:int f4(){return f3();};
    public:int randomNumber=((双)rand()/(rand_MAX))*10;
    };
    B类:公共A{
    public:virtualint f2(){return randomNumber*4;};
    public:virtualint f3()final{return randomNumber*4;};
    };
    int main(){
    std::向量bs;
    常数int numTest=10000;
    
    对于(int n=0;n而言,问题在于在您的示例中没有
    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编译为与语言无关的中间形式(分别为格式和格式)然后这就变成了一个别名问题,编译器必须静态地证明虚拟表中的条目始终是
    B::f3
    。通常不能确定,不幸的是,关于最终方法的信息似乎传播得不够远。GCC至少在有利可图的情况下会这样做

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

    实现f4的一种方法是,编译器生成您想要的代码变体,而无需您重复自己的代码变体,该方法将作为一个模板函数,位于:

    template <typename DerivedFromA>
    inline int f4(DerivedFromA &x)
    {
      return x.f3();
    }
    
    模板
    内联整数f4(从A&x派生)
    {
    返回x.f3();
    }
    
    您应该尝试将
    B
    声明为final。也许编译器会尝试对某些调用进行Device。但一般来说,您不应该过分依赖编译器。@VTT没有帮助,
    f4()变得更昂贵。使用RexTealter,@ MARS,根据VisualC++ ++代码SooCy.Fiel.CPO-O2-AEXE/ EHsc / MD /I C:\BooStY116000/Loo/LBPATE:C:\BooSTy1160600\ListB/<代码>结果与非最终版本相比有所改进。谢谢。1。eem能够做到这一点
    您的意思是演示中的
    f4()
    在gcc中应该是快速的吗?一个证据(使用的时间,gcc)显示它不能内联。这与你的说法相矛盾。但我不确定rextester是2017年的。你在你的计算机或在线网站上测试过它吗?2.
    gcc和LLVM编译成一种语言不可知的中间形式
    :你能提供一个参考吗?这对我来说是新知识。1.我在gcc.godbolt.com上用一个简化的exa尝试过它示例。但是,我犯了一个错误,使用了B的局部变量。然后编译器知道的比使用B指针时知道的要多。请参见此示例:。在那里,只执行了推测性的设备化。我将纠正这一点。2.我添加了指向各自格式说明的链接。我相信,通过谷歌搜索,您会发现更好的介绍奥斯。