【问题标题】:NVI and devirtualisationNVI 和去虚拟化
【发布时间】:2013-08-16 10:55:17
【问题描述】:

如果您使用的是 NVI,编译器能否对函数调用进行去虚拟化?

一个例子:

#include <iostream>

class widget
{
public:
    void foo() { bar(); }

private:
    virtual void bar() = 0;
};

class gadget final : public widget
{
private:
    void bar() override { std::cout << "gadget\n"; }
};

int main()
{
    gadget g;
    g.foo();    // HERE.
}

在标记的那一行,编译器能否对bar的调用进行虚拟化?

【问题讨论】:

    标签: c++ optimization c++11 virtual-functions


    【解决方案1】:

    鉴于已知g 的动态类型恰好是gadget,编译器可以在内联foo 后对bar 的调用去虚拟化,而不管finalclass gadget 上的用法声明或gadget::bar的声明。我将分析这个不使用 iostreams 的类似程序,因为输出程序集更易于阅读:

    class widget
    {
    public:
        void foo() { bar(); }
    
    private:
        virtual void bar() = 0;
    };
    
    class gadget : public widget
    {
        void bar() override { ++counter; }
    public:
        int counter = 0;
    };
    
    int test1()
    {
        gadget g;
        g.foo();
        return g.counter;
    }
    
    int test2()
    {
        gadget g;
        g.foo();
        g.foo();
        return g.counter;
    }
    
    int test3()
    {
        gadget g;
        g.foo();
        g.foo();
        g.foo();
        return g.counter;
    }
    
    int test4()
    {
        gadget g;
        g.foo();
        g.foo();
        g.foo();
        g.foo();
        return g.counter;
    }
    
    int testloop(int n)
    {
        gadget g;
        while(--n >= 0)
            g.foo();
        return g.counter;
    }
    

    我们可以通过检查输出程序集来确定去虚拟化是否成功:(GCC)(clang)。两者都将test 优化为等效于return 1; - 调用被去虚拟化和内联,并且对象被消除。 Clang does the same for test2 through test4 - return 2; / 3 / 4 - 但GCC seems to gradually lose track of the type information the more times it must perform the optimization。尽管成功地将test1 优化为常量的返回,test2 大致变为:

    int test2() {
        gadget g;
        g.counter = 1;
        g.gadget::bar();
        return g.counter;
    }
    

    第一个调用已经被去虚拟化并且它的效果被内联(g.counter = 1),但是第二个被仅仅去虚拟化了。在test3 中添加附加调用会导致:

    int test3() {
        gadget g;
        g.counter = 1;
        g.gadget::bar();
        g.bar();
        return g.counter;
    }
    

    第一次调用是完全内联的,第二次只是去虚拟化,但第三次调用根本没有优化。这是来自虚拟表和间接函数调用的简单 Jane 负载。 test4 中的附加调用结果相同:

    int test4() {
        gadget g;
        g.counter = 1;
        g.gadget::bar();
        g.bar();
        g.bar();
        return g.counter;
    }
    

    值得注意的是,两个编译器都没有将testloop 的简单循环中的调用去虚拟化,它们都编译为等价于:

    int testloop(int n) {
      gadget g;
      while(--n >= 0)
        g.bar();
      return g.counter;
    }
    

    甚至在每次迭代时从对象重新加载 vtable 指针。

    final 标记添加到class gadget 声明和gadget::bar 定义不会影响任一编译器(GCC) (clang) 生成的程序集输出。

    影响生成程序集的 是删除 NVI。这个程序:

    class widget
    {
    public:
        virtual void bar() = 0;
    };
    
    class gadget : public widget
    {
    public:
        void bar() override { ++counter; }
        int counter = 0;
    };
    
    int test1()
    {
        gadget g;
        g.bar();
        return g.counter;
    }
    
    int test2()
    {
        gadget g;
        g.bar();
        g.bar();
        return g.counter;
    }
    
    int test3()
    {
        gadget g;
        g.bar();
        g.bar();
        g.bar();
        return g.counter;
    }
    
    int test4()
    {
        gadget g;
        g.bar();
        g.bar();
        g.bar();
        g.bar();
        return g.counter;
    }
    
    int testloop(int n)
    {
        gadget g;
        while(--n >= 0)
            g.bar();
        return g.counter;
    }
    

    由两个编译器 (GCC) (clang) 完全优化为:

    int test1()
    { return 1; }
    
    int test2()
    { return 2; }
    
    int test3()
    { return 3; }
    
    int test4()
    { return 4; }
    
    int testloop(int n)
    { return n >= 0 ? n : 0; }
    

    总而言之,尽管编译器可以bar 的调用进行去虚拟化,但在存在 NVI 的情况下它们可能并不总是这样做。在当前的编译器中,优化的应用并不完善。

    【讨论】:

    • +1 用于出色的装配侦探工作。更令人惊讶的是调用 gadget* g = new gadget(); g-&gt;foo();widget* g = new gadget(); g-&gt;foo(); 是如何被 gcc 和 Clang 处理的。
    【解决方案2】:

    理论上是的——但这与 NVI 无关。在您的示例中,编译器理论上也可以对调用 g.bar() 进行去虚拟化。编译器唯一需要知道的是该对象是否真的是 gadget 类型或者它可能是其他东西。如果编译器可以推断它只能是 g 类型,它可以对调用进行去虚拟化。

    但可能大多数编译器不会尝试。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-01-21
      • 1970-01-01
      • 2011-05-17
      • 2020-09-08
      • 1970-01-01
      相关资源
      最近更新 更多