NVI和错失良机
NVI and devirtualisation
如果您使用的是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 << "gadgetn"; }
};
int main()
{
gadget g;
g.foo(); // HERE.
}
在标记的那一行,编译器是否可以取消对bar
的调用?
已知g
的动态类型正是gadget
,编译器可以在内联foo
之后取消对bar
的调用,而不管class gadget
声明或gadget::bar
声明中使用了final
。我将分析这个不使用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分别对test2
到test4
-return 2;
/3/4执行相同的操作,但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的情况下,它们可能并不总是这样做。优化在当前编译器中的应用还不完善。
理论上是的,但这与NVI无关。在您的示例中,编译器理论上也可以对调用g.bar()
进行去虚拟化。编译器唯一需要知道的是,对象是否真的是gadget类型,或者可能是其他类型。如果编译器可以推断它只能是g类型,那么它可以对调用进行去虚拟化。
但可能大多数编译器都不会尝试。