の動的な型g
が正確に であることがわかっている場合、コンパイラは、宣言または の宣言での の使用に関係なく、インライン展開後gadget
に への呼び出しを非仮想化できます。出力アセンブリが読みやすいため、iostream を使用しないこの同様のプログラムを分析します。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 はそれぞれ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
) が、2 番目の呼び出しは非仮想化されているだけです。追加の呼び出しを追加すると、次のようになりtest3
ます。
int test3() {
gadget g;
g.counter = 1;
g.gadget::bar();
g.bar();
return g.counter;
}
繰り返しますが、最初の呼び出しは完全にインライン化され、2 番目の呼び出しは非仮想化されているだけですが、3 番目の呼び出しはまったく最適化されていません。これは、仮想テーブルと間接関数呼び出しからの単純な 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
の両方にマーカーを追加しても、コンパイラ(GCC) (clang)によって生成されるアセンブリ出力には影響しません。class gadget
gadget::bar
生成されたアセンブリに影響を与えるのは、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が存在する場合は常にそうするとは限りません。最適化の適用は、現在のコンパイラでは不完全です。