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に への呼び出しを非仮想化できます。出力アセンブリが読みやすいため、iostream を使用しないこの同様のプログラムを分析します。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)を調べることで、非仮想化の成功を判断できます。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 gadgetgadget::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が存在する場合は常にそうするとは限りません。最適化の適用は、現在のコンパイラでは不完全です。

于 2013-08-16T14:31:47.830 に答える
3

理論的にはそうですが、それは NVI とは関係ありません。あなたの例では、コンパイラは理論的に呼び出しg.bar()を非仮想化することもできます。コンパイラが知る必要がある唯一のことは、オブジェクトが本当にガジェット タイプであるか、それとも別のものであるかどうかです。コンパイラが g 型のみであると推論できる場合、呼び出しを非仮想化できます。

しかし、おそらく、ほとんどのコンパイラは試行しません。

于 2013-08-16T11:42:48.220 に答える