C#およびJava実装では、オブジェクトには通常、そのクラスへの単一のポインターがあります。これは、単一継承言語であるため可能です。クラス構造には、単一継承階層のvtableが含まれます。しかし、インターフェイスメソッドの呼び出しには、多重継承の問題もすべてあります。これは通常、実装されたすべてのインターフェイスの追加のvtableをクラス構造に入れることで解決されます。これにより、C ++の一般的な仮想継承の実装と比較してスペースが節約されますが、インターフェイスメソッドのディスパッチがより複雑になります。これは、キャッシングによって部分的に補うことができます。
たとえば、OpenJDK JVMでは、各クラスには、実装されたすべてのインターフェースのvtableの配列が含まれています(インターフェースvtableはitableと呼ばれます)。インターフェイスメソッドが呼び出されると、このインターフェイスのitableをこの配列で線形的に検索し、そのitableを介してメソッドをディスパッチできます。キャッシュは、各呼び出しサイトがメソッドディスパッチの結果を記憶するように使用されるため、この検索は、具体的なオブジェクトタイプが変更された場合にのみ繰り返す必要があります。メソッドディスパッチの擬似コード:
// Dispatch SomeInterface.method
Method const* resolve_method(
Object const* instance, Klass const* interface, uint itable_slot) {
Klass const* klass = instance->klass;
for (Itable const* itable : klass->itables()) {
if (itable->klass() == interface)
return itable[itable_slot];
}
throw ...; // class does not implement required interface
}
(OpenJDK HotSpot インタープリターまたはx86コンパイラーの実際のコードを比較します。)
C#(より正確にはCLR)は、関連するアプローチを使用します。ただし、ここではitablesにはメソッドへのポインターは含まれませんが、スロットマップです。クラスのメインvtable内のエントリを指します。Javaの場合と同様に、正しいitableを検索する必要があるのは最悪のシナリオに過ぎず、呼び出しサイトでキャッシュすることでほぼ常にこの検索を回避できることが期待されます。CLRはVirtual Stub Dispatchと呼ばれる手法を使用して、JITでコンパイルされたマシンコードにさまざまなキャッシュ戦略を適用します。擬似コード:
Method const* resolve_method(
Object const* instance, Klass const* interface, uint interface_slot) {
Klass const* klass = instance->klass;
// Walk all base classes to find slot map
for (Klass const* base = klass; base != nullptr; base = base->base()) {
// I think the CLR actually uses hash tables instead of a linear search
for (SlotMap const* slot_map : base->slot_maps()) {
if (slot_map->klass() == interface) {
uint vtable_slot = slot_map[interface_slot];
return klass->vtable[vtable_slot];
}
}
}
throw ...; // class does not implement required interface
}
OpenJDK-pseudocodeとの主な違いは、OpenJDKでは各クラスに直接または間接的に実装されたすべてのインターフェイスの配列があるのに対して、CLRはそのクラスに直接実装されたインターフェイスのスロットマップの配列のみを保持することです。したがって、スロットマップが見つかるまで、継承階層を上に向かって歩く必要があります。深い継承階層の場合、これによりスペースが節約されます。これらは、ジェネリックの実装方法が原因でCLRに特に関連します。ジェネリック特化の場合、クラス構造がコピーされ、メインvtableのメソッドがスペシャライゼーションに置き換えられます。スロットマップは引き続き正しいvtableエントリをポイントするため、クラスのすべての汎用特殊化の間で共有できます。
最後に、インターフェイスのディスパッチを実装する可能性がさらにあります。オブジェクトまたはクラス構造にvtable / itableポインターを配置する代わりに、基本的にはペアであるオブジェクトへのファットポインターを使用できます(Object*, VTable*)
。欠点は、これによりポインターのサイズが2倍になり、アップキャスト(具象型からインターフェイス型へ)が解放されないことです。しかし、より柔軟で、間接性が少なく、クラスから外部にインターフェースを実装できることも意味します。関連するアプローチは、Goインターフェース、Rust特性、およびHaskell型クラスによって使用されます。
参考資料と詳細資料:
- ウィキペディア:インラインキャッシュ。高価なメソッド検索を回避するために使用できるキャッシュアプローチについて説明します。通常、vtableベースのディスパッチには必要ありませんが、上記のインターフェイスディスパッチ戦略のようなより高価なディスパッチメカニズムには非常に望ましいです。
- OpenJDK Wiki(2013):インターフェース呼び出し。itablesについて説明します。
- Pobar、Neward(2009):SSCLI 2.0内部。本の第5章では、スロットマップについて詳しく説明しています。決して公開されませんでしたが、著者のブログで利用可能になりました。PDFリンクは以来、移動しました。この本はおそらくCLRの現在の状態を反映していないでしょう。
- CoreCLR(2006):仮想スタブ派遣。In:Book Of The Runtime。高価な検索を回避するために、スロットマップとキャッシュについて説明します。
- Kennedy、Syme(2001):.NET共通言語ランタイムのジェネリックの設計と実装。(PDFリンク)。ジェネリックを実装するためのさまざまなアプローチについて説明します。メソッドは特殊化される可能性があるため、vtablesを書き換える必要があるため、ジェネリックはメソッドディスパッチと対話します。