NVI和虚拟化

Sim*_*ple 4 c++ optimization virtual-functions c++11

如果你正在使用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.
}
Run Code Online (Sandbox Code Playgroud)

在标记的行上,编译器可以将调用虚拟化为bar

Cas*_*sey 5

鉴于动态类型g是已知的准确gadget,编译器可以devirtualize调用bar内联后foo,不管使用的finalclass gadget申报或申报gadget::bar.我将分析这个不使用iostream的类似程序,因为输出程序集更容易阅读:

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;
}
Run Code Online (Sandbox Code Playgroud)

我们可以通过检查输出程序集来确定虚拟化的成功:(GCC),(clang).两者都优化test为相当于return 1;- 调用是虚拟化和内联的,并且消除了对象.Clangtest2test4return 2;分别通过 -/3/4 进行相同的操作 - 但是GCC似乎逐渐失去对类型信息的跟踪,它必须执行优化的次数越多.尽管成功优化test1了常量的返回,但test2大致变为:

int test2() {
    gadget g;
    g.counter = 1;
    g.gadget::bar();
    return g.counter;
}
Run Code Online (Sandbox Code Playgroud)

第一个调用已经被虚拟化并且其效果是inlined(g.counter = 1),但第二个调用只是被虚拟化.在test3结果中添加附加调用:

int test3() {
    gadget g;
    g.counter = 1;
    g.gadget::bar();
    g.bar();
    return g.counter;
}
Run Code Online (Sandbox Code Playgroud)

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

int test4() {
    gadget g;
    g.counter = 1;
    g.gadget::bar();
    g.bar();
    g.bar();
    return g.counter;
}
Run Code Online (Sandbox Code Playgroud)

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

int testloop(int n) {
  gadget g;
  while(--n >= 0)
    g.bar();
  return g.counter;
}
Run Code Online (Sandbox Code Playgroud)

甚至在每次迭代时从对象重新加载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;
}
Run Code Online (Sandbox Code Playgroud)

完全由两个编译器(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; }
Run Code Online (Sandbox Code Playgroud)

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