web-dev-qa-db-ja.com

コンパイラーは仮想呼び出しに対してこの最適化を行いますか?

これは思いついたばかりであり、これをどのように検索するか本当にわかりません。

次のクラスがあるとします

_class A
{
public:
    virtual void Foo() = 0;

    virtual void ManyFoo(int N) 
    {
        for (int i = 0; i < N; ++i) Foo();
    } 
};

class B : public A
{
public:
    virtual void Foo()
    {
        // Do something
    }
};
_

コンパイラは、ManyFoo()の呼び出しをインライン化するBのバージョンのB::Foo()を作成しますか?

そうでない場合、Bを最終クラスにすると、この最適化が有効になりますか?

編集:私はこれが仮想呼び出しのどこで行われたのか(特に、ManyFoo()への呼び出し全体がインライン化されている場合は別として)疑問に思っていました。

2
Bwmat

あなたが探している用語は「非正規化」だと思います。

とにかく試してみましたか?もし その例をCompiler Explorerに入れます

extern void extCall ();

class A
{
public:
    virtual void Foo() const = 0;

    virtual void ManyFoo(int N) const
    {
        for (int i = 0; i < N; ++i) Foo();
    } 
};

class B final : public A
{
public:
    virtual void Foo() const
    {
        extCall ();
    }
};

void b_value_foo (B b) {
    b.ManyFoo (6);
}

void b_ref_foo (B const & b) {
    b.ManyFoo (6);
}

void b_indirect_foo (B b) {
    b_ref_foo (b);
}

... GCCは-Osを使用して以下を生成できます。

b_value_foo(B):
        Push    rax
        call    extCall()
        call    extCall()
        call    extCall()
        call    extCall()
        call    extCall()
        pop     rdx
        jmp     extCall()
b_ref_foo(B const&):
        mov     rax, QWORD PTR [rdi]
        mov     esi, 6
        mov     rax, QWORD PTR [rax+8]
        jmp     rax
b_indirect_foo(B):
        jmp     b_ref_foo(B const&)

オブジェクトの具象タイプが100%わかっている場合は、仮想呼び出しを通じてインライン化しますb(nb -Os-O2に変更すると、完全にインライン化されますb_indirect_foo)。しかし、それはインスタンスにトレースすることができない参照によってのみ見ることができるオブジェクトの具体的なタイプを確信できず、これを無効にするfinalアノテーションを信頼していないようです(おそらくこれは非常にABIに脆弱であるためです。私は個人的にはそれを望んでいません)。 ただし、メンバー関数のfinal注釈を信頼します ですが、この例では、その構造によりそれを排除しています。

GCCでは、いくつかのバージョンでこの最適化が行われています。この場合、ClangとMSVCはそれを行わないようです(ただし、機能を宣伝します)。そのため、例とコンパイラーの間で明らかに能力が大きく異なります。

6
Leushenko

This-> ManyFoo()がA内の実装を呼び出したとき、this-> Foo()の実装もA内の実装となるのは良い推測ですが、必ずしもそうであるとは限りません。したがって、コンパイラは疑似コードを生成できます。このようなManyFoo:

if (&this->Foo == &A->Foo) {
    for (int i = 0; i < N; ++i)
        inlined A->Foo();
} else {
    for (int i = 0; i < N; ++i)
        virtual this->Foo();
}

コンパイラーは、this-> Foo()のアドレスを一度取得してから、より高速の場合、this-> Foo()の代わりにその関数ポインターを呼び出すこともできます。コンパイラは、ManyFoo()内のFoo()への呼び出しをインライン化することもできます。Foo()がオーバーロードされている場合は常に、新しいバージョンのManyFoo()を作成します。

私はJava実行時にインライン化するものを決定するVMを確認しました。通常は呼び出される実装をしばらく追跡し、インライン化します(もちろん安全な方法で、したがって別の実装の場合) Fooが呼び出された場合、それは機能しますが、遅くなります。したがって、99%のケースでC-> Foo()を呼び出した場合、そのケースがチェックされ、インライン化されます。これは、クラスC用のインラインバージョンとクラスD用のイン​​ラインバージョン。

1
gnasher729

はい、そうです。メソッドはインラインで定義されるので、インライン化できる場合があります。ただし、ClangもGCCも特殊なB::ManyFoo(int)を作成しません。

不適切な最適化を防ぐためにコードを修正し、いくつかの動作を示します。

_struct A {
    virtual int Foo() = 0;
    virtual int ManyFoo(int N)  {
        int res = 0;
        for (int i = 0; i < N; ++i) res += Foo();
        return res;
    } 
};

struct B : A {
    virtual int Foo() { return 3; }
};

B force_code_generation() { return {}; }

int dynamic_dispatch(A& object) { return object.ManyFoo(2); }
int static_dispatch (B value)   { return value .ManyFoo(2); }
_

コンパイラが静的ディスパッチを実行できる非仮想呼び出しサイトで、ManyFoo()およびFoo()は、完全に平らになります。 -O2を使用するGCC 8.2は、コンパイル時に関数を評価できます。

_mov eax, 6
ret
_

しかし、Clangはその最適化を行っていないようです。 ManyFoo(2)を呼び出すために仮想呼び出しを使用するFoo()呼び出しをインライン化するだけです。疑似コード:

_static_dispatch(B* rdi):
        Push    rbp
        Push    rbx
        Push    rax

        rbx = rdi

        rax = *rbx  // load vtable
        eax = call rax[0](rdi)  // first Foo() call
        ebp = eax

        rax = *rbx  // load vtable
        rdi = rbx  // move this pointer to rdi
        eax = call rax[0](rdi)
        eax += ebp  // add the Foo() results

        rsp += 8  // discard saved rax
        pop     rbx
        pop     rbp
        return eax
_

動的ディスパッチでは、これらの最適化は一般的には不可能です。 Clangは特別な最適化を追加せず、通常の仮想呼び出しを使用します。ただし、GCC 8.2では、オプションで仮想関数をインライン化するために、仮想コールサイトにガードが追加されています。以下は、生成されたアセンブリを疑似コードとして書き直し、わかりやすくするために並べ替えたものです。

_dynamic_dispatch(A& rdi):
        rax = *rdi  // load vtable from object
        rdx = rax[8]  // load ManyFoo(int) vtable entry
        // check if ManyFoo(int) method is A::ManyFoo(int)
        if (rdx != &A::ManyFoo(int)) {
            // fallback for virtual ManyFoo(2) call, and return
            esi = 2
            goto rdx  // tailcall
        }

        // We are now in the specialized A::ManyFoo(2) version.
        // The loop for N=2 is unrolled.
        Push    rbp
        Push    rbx
        rsp -= 8

        // first Foo() call:
        // check if Foo() is B::Foo(), else fall back to virtual call
        ebx = 3  // result of the first B::Foo() call if it is inlined
        rax = rax[0]  // load Foo() vtable entry
        if (rax != &B::Foo()) {
            // fallback for first virtual Foo() call
            rbp = rdi
            eax = call rax(rdi)
            ebx = eax  // save result of first call

            // second Foo() call:
            // check again if Foo() is B::Foo()
            // Can "this" even change its type???
            rax = *rbp
            rax = rax[0]
            if (rax != &B::Foo()) {
                // fallback for second virtual Foo() call
                rdi = rbp
                eax = call rax(rdi)
                goto end
            }
        }

        eax = 3  // result of second B::Foo() call

end:
        // add the result of the calls and return
        eax += ebx
        rsp += 8
        pop     rbx
        pop     rbp
        return eax
_

ClangもGCCも、Bfinalであるかどうかに応じて、生成されたコードを変更しません。

ソース: Godboltコンパイラエクスプローラーでアセンブリを表示

0
amon