4

If you're using NVI can the compiler devirtualise function calls?

An example:

#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.
}

At the line marked can the compiler devirtualise the call to bar?

4

2 回答 2

5

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

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)。两者都优化testreturn 1;- 调用被去虚拟化和内联,并且对象被消除。Clang 分别为 through-/3/4 做同样的test2事情test4return 2;GCC 似乎逐渐失去对类型信息的跟踪,它必须执行优化的次数越多。尽管成功地优化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 的情况下它们可能并不总是这样做。在当前的编译器中,优化的应用是不完善的。

于 2013-08-16T14:31:47.830 回答
3

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

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

于 2013-08-16T11:42:48.220 回答