これは思いついたばかりであり、これをどのように検索するか本当にわかりません。
次のクラスがあるとします
_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()
への呼び出し全体がインライン化されている場合は別として)疑問に思っていました。
あなたが探している用語は「非正規化」だと思います。
とにかく試してみましたか?もし その例を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はそれを行わないようです(ただし、機能を宣伝します)。そのため、例とコンパイラーの間で明らかに能力が大きく異なります。
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用のインラインバージョン。
はい、そうです。メソッドはインラインで定義されるので、インライン化できる場合があります。ただし、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も、B
がfinal
であるかどうかに応じて、生成されたコードを変更しません。