鉴于已知的动态类型g
是精确的,编译器可以在内联gadget
后将调用去虚拟化,而不管在声明上或在声明上使用。我将分析这个不使用 iostreams 的类似程序,因为输出程序集更易于阅读:bar
foo
final
class gadget
gadget::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)。两者都优化test
为return 1;
- 调用被去虚拟化和内联,并且对象被消除。Clang 分别为 through-/3/4 做同样的test2
事情test4
,return 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 的情况下它们可能并不总是这样做。在当前的编译器中,优化的应用是不完善的。