In C # - und Java-Implementierungen haben die Objekte normalerweise einen einzelnen Zeiger auf ihre Klasse. Dies ist möglich, da es sich um Sprachen mit einfacher Vererbung handelt. Die Klassenstruktur enthält dann die vtable für die Einfachvererbungshierarchie. Der Aufruf von Interface-Methoden birgt jedoch auch alle Probleme der Mehrfachvererbung. Dies wird normalerweise gelöst, indem zusätzliche vtables für alle implementierten Schnittstellen in die Klassenstruktur eingefügt werden. Dies spart im Vergleich zu typischen virtuellen Vererbungsimplementierungen in C ++ Platz, erschwert jedoch den Versand von Schnittstellenmethoden - was teilweise durch Caching kompensiert werden kann.
In der OpenJDK-JVM enthält jede Klasse ein Array von vtables für alle implementierten Schnittstellen (eine Schnittstellen-vtable wird itable genannt ). Wenn eine Schnittstellenmethode aufgerufen wird, wird dieses Array linear nach der Itable dieser Schnittstelle durchsucht, und die Methode kann über diese Itable verteilt werden. Das Caching wird verwendet, damit sich jeder Aufrufstandort das Ergebnis des Methodenversands merkt, sodass diese Suche nur wiederholt werden muss, wenn sich der konkrete Objekttyp ändert. Pseudocode für den Methodenversand:
// 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
}
(Vergleichen Sie den tatsächlichen Code im OpenJDK HotSpot- Interpreter oder im x86-Compiler .)
C # (oder genauer gesagt die CLR) verwendet einen verwandten Ansatz. In diesem Fall enthalten die Tabellen jedoch keine Zeiger auf die Methoden, sondern sind Slotmaps: Sie verweisen auf Einträge in der Haupttabelle der Klasse. Wie bei Java ist die Suche nach der korrekten itable nur das Worst-Case-Szenario, und es wird erwartet, dass das Caching am Aufrufstandort diese Suche fast immer vermeiden kann. Die CLR verwendet eine Technik namens Virtual Stub Dispatch, um den JIT-kompilierten Maschinencode mit verschiedenen Caching-Strategien zu patchen. Pseudocode:
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
}
Der Hauptunterschied zum OpenJDK-Pseudocode besteht darin, dass in OpenJDK jede Klasse ein Array aller direkt oder indirekt implementierten Schnittstellen enthält, während die CLR nur ein Array von Slotmaps für Schnittstellen enthält, die direkt in dieser Klasse implementiert wurden. Wir müssen daher die Vererbungshierarchie nach oben durchlaufen, bis eine Slot-Map gefunden wird. Bei Hierarchien mit tiefer Vererbung führt dies zu Platzeinsparungen. Diese sind in CLR aufgrund der Art und Weise, wie Generika implementiert werden, besonders relevant: Bei einer generischen Spezialisierung wird die Klassenstruktur kopiert und Methoden in der Haupttabelle können durch Spezialisierungen ersetzt werden. Die Slotmaps zeigen weiterhin auf die richtigen vtable-Einträge und können daher von allen allgemeinen Spezialisierungen einer Klasse gemeinsam genutzt werden.
Abschließend gibt es noch weitere Möglichkeiten, den Interface-Versand zu implementieren. Anstatt den Zeiger vtable / itable im Objekt oder in der Klassenstruktur zu platzieren, können wir fette Zeiger auf das Objekt verwenden, die im Grunde genommen ein (Object*, VTable*)
Paar sind. Der Nachteil ist, dass dies die Größe von Zeigern verdoppelt und Upcasts (von einem konkreten Typ zu einem Schnittstellentyp) nicht frei sind. Aber es ist flexibler, hat weniger Indirektion und bedeutet auch, dass Interfaces außerhalb einer Klasse implementiert werden können. Verwandte Ansätze werden von Go-Interfaces, Rust-Merkmalen und Haskell-Typenklassen verwendet.
Referenzen und weiterführende Literatur:
- Wikipedia: Inline-Caching . Erläutert Caching-Ansätze, mit denen teure Methodensuchen vermieden werden können. In der Regel nicht erforderlich für vtable-basierten Versand, aber sehr wünschenswert für teurere Versandmechanismen wie die obigen Schnittstellen-Versandstrategien.
- OpenJDK Wiki (2013): Schnittstellenaufrufe . Bespricht itables.
- Pobar, Neward (2009): SSCLI 2.0 Internals. In Kapitel 5 des Buches werden Slot-Maps sehr detailliert behandelt. Wurde nie veröffentlicht, sondern von den Autoren auf ihren Blogs zur Verfügung gestellt . Der PDF-Link ist inzwischen umgezogen. Dieses Buch spiegelt wahrscheinlich nicht mehr den aktuellen Stand der CLR wider.
- CoreCLR (2006): Virtual Stub Dispatch . In: Buch der Laufzeit. Erläutert Slot Maps und Caching, um teure Lookups zu vermeiden.
- Kennedy, Syme (2001): Design und Implementierung von Generics für die .NET Common Language Runtime . ( PDF-Link ). Erläutert verschiedene Ansätze zur Implementierung von Generika. Generics interagieren mit dem Methodenversand, da Methoden möglicherweise spezialisiert sind und vtables möglicherweise neu geschrieben werden müssen.