NVI和虚拟化

时间:2013-08-16 10:55:17

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

如果你正在使用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

2 个答案:

答案 0 :(得分:5)

鉴于已知g的动态类型正好是gadget,编译器可以在内联bar后将调用虚拟化为foo,无论其使用情况如何关于final声明或class 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;
}

我们可以通过检查输出程序集来确定虚拟化是否成功:(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 :(得分:3)

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

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