鉴于已知g 的动态类型恰好是gadget,编译器可以在内联foo 后对bar 的调用去虚拟化,而不管final 在class gadget 上的用法声明或gadget::bar的声明。我将分析这个不使用 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 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 的情况下它们可能并不总是这样做。在当前的编译器中,优化的应用并不完善。