
2026/02/26 3:41
**デヴァーチャリゼーションと静的ポリモルフィズム**
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
このテキストは、仮想ディスパッチが隠れた実行時オーバーヘッドを追加することを説明しています:ポインタ間接参照、大きなオブジェクトレイアウト、およびインライン化の機会が減少します。仮想呼び出しは実行時に vptr を検索しますが、非仮想呼び出しは直接解決されます ― これは
が仮想か非仮想かでアセンブリを対比することで示されています。foo()
デヴァーチャライズ(仮想化解除)は、コンパイラが具体的な型(例:ローカルのオブジェクトやDerivedメソッド)を知っている場合にのみ発生します。クロス・トランスレーション・ユニット境界は通常デヴァーチャライズを阻止しますが、全体プログラムフラグ(final)とリンク時最適化(-fwhole-program)により、多くの呼び出しが解決可能になります。-flto
CRTP(Curiously Recurring Template Pattern)による静的多態性は vtable を完全に排除します;コンパイラはすべての呼び出しをインライン化し、結果を定数畳み込みできます(例:は単一のDerived::foo()にコンパイルされます)。トレードオフとしては、各インスタンス(mov eax, 165)が独自の型となり、アップキャスト用に共通の実行時ベースを持たなくなる点です。Base<Derived>
C++23 の「deducing‑this」構文は、クラス全体をテンプレート化せずに同様のコンパイル時ディスパッチを提供し、CRTP の使用を簡素化します。両ケースとも最適化されたアセンブリには vptr や間接呼び出しが現れません。将来のコンパイラ改善によりデヴァーチャライズがさらに自動化され、静的多態性は性能クリティカルなコードで魅力的な選択肢となるでしょう。
本文
「クリーン」なポリモーフィック設計が遅くなる理由
- 仮想ディスパッチは実行時ポリモーフィズムを提供しますが、隠れたコストも伴います。
- ポインタの間接参照
- オブジェクトレイアウトの肥大化(vptr)
- インライン展開の機会減少
コンパイラはデバーチャル化を試みますが、常に可能というわけではありません。
レイテンシーに敏感なコードパスでは、動的ディスパッチを静的ポリモーフィズムに置き換えることで、呼び出しをコンパイル時に解決でき、抽象化のランタイムコストをほぼゼロにできます。
仮想ディスパッチ
ベースインタフェースが仮想メソッドを公開し、派生クラスでオーバーライドされるときに起こる実行時ポリモーフィズムです。
から呼び出すと、その時点で適切なオーバーライドへディスパッチされます。Base&
内部構造は次のようになります。
virtual table (vtable) → クラスごとに1つ pointer to vtable (vptr) → 各インスタンスに格納
アセンブリ例
; 非仮想 foo() bar(Base* base): sub rsp, 8 call Base::foo() ; 直接呼び出し add rsp, 8 add eax, 77 ret ; 仮想 foo() bar(Base* base): sub rsp, 8 mov rax, [rdi] ; vptr をロード call [rax] ; vtable 経由で間接呼び出し add rsp, 8 add eax, 77 ret
追加の
vptr はオブジェクトサイズを増やし、間接参照によりインライン化が妨げられ、分岐ミスプリダクションやキャッシュ効率低下を招きます。
デバーチャル化(Devirtualisation)
コンパイラが呼び出されるオーバーライドを推論できる場合、vtable を使わずに直接呼び出しを生成します。動的型が固定の場合は簡単です。
struct Base { virtual int foo() = 0; }; struct Derived : Base { int foo() override { return 77; } }; int bar() { Derived d; return d.foo(); // コンパイラは Derived::foo を知っている }
制限事項
- 従来のコンパイル方式では、翻訳単位(TU)ごとにオブジェクトファイルが生成されます。リンカがそれらを結合するため、クロス‑TU 最適化は限定的です。
- コンパイラフラグで助けることができます:
– 現在の TU がプログラム全体であると仮定し、他に派生クラスが存在しないと見なします。-fwhole-program
(リンク時最適化) – オブジェクトファイルに中間表現を保持し、すべての TU を横断して最適化を行います。-flto
の使用final
class Base { public: virtual int foo(); virtual int bar(); }; class Derived : public Base { public: int foo() override; int bar() final; // 以降オーバーライド不可 }; int test(Derived* d) { return d->foo() + d->bar(); }
bar() は仮想で宣言されているにも関わらず、直接呼び出しとして生成されます。
test(Derived*): push rbx sub rsp, 16 mov rax, [rdi] ; vptr をロード mov [rsp+8], rdi ; d を保存 call [rax] ; foo() の仮想呼び出し mov rdi, [rsp+8] mov ebx, eax call Derived::bar() ; 直接呼び出し add rsp, 16 add eax, ebx pop rbx ret
静的ポリモーフィズム
デバーチャル化が不可能な場合は静的ポリモーフィズムを利用します。代表的な手法は CRTP(Curiously Recurring Template Pattern) です。
template<class Derived> class Base { public: int foo() { return 77 + static_cast<Derived*>(this)->bar(); } }; class Derived : public Base<Derived> { public: int bar() { return 88; } }; int test() { Derived d; return d.foo(); }
-O3 を付けてコンパイルすると、すべてがインライン化されます。
test(): mov eax, 165 ; 77 + 88 ret
vtable も vptr もなく、間接参照はありません。
トレードオフ
Base<Derived> の各インスタンスは別個の型として扱われます。共通のランタイムベースがないため、共有機能はすべてテンプレート化する必要があります。
C++23「deducing this」
C++23 で導入された deducing this は静的ディスパッチを保ちつつ、書きやすさを向上させます。
class Base { public: int foo(this auto&& self) { return 77 + self.bar(); } }; class Derived : public Base { public: int bar() { return 88; } };
foo は foo<Derived> としてインスタンス化され、bar() の呼び出しは静的に解決・インライン化されます。結果として生成されるコードは CRTP を使った場合と同等です。