Obwohl dies im C ++ - Standard nicht vorgeschrieben ist, sieht es so aus, als ob GCC beispielsweise übergeordnete Klassen, einschließlich rein abstrakter Klassen, implementiert, indem es in jeder Instanziierung der betreffenden Klasse einen Zeiger auf die v-Tabelle für diese abstrakte Klasse einfügt .
Dies erhöht natürlich die Größe jeder Instanz dieser Klasse um einen Zeiger für jede übergeordnete Klasse.
Aber ich habe bemerkt, dass viele C # -Klassen und -Strukturen viele übergeordnete Schnittstellen haben, die im Grunde genommen reine abstrakte Klassen sind. Es würde mich wundern, wenn jede Instanz von say Decimal
mit 6 Zeigern auf die verschiedenen Schnittstellen aufgebläht wäre.
Also, wenn C # Schnittstellen anders macht, wie macht es diese, zumindest in einer typischen Implementierung (ich verstehe, dass der Standard selbst eine solche Implementierung möglicherweise nicht definiert)? Und haben irgendwelche C ++ - Implementierungen eine Möglichkeit, das Aufblähen der Objektgröße zu vermeiden, wenn Sie Klassen reine virtuelle Eltern hinzufügen?
quelle
IComparer
mitCompare
g++-7 -fdump-class-hierarchy
Ausgabe betrachtete.Antworten:
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:
(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:
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:
quelle
callvirt
AKACEE_CALLVIRT
in CoreCLR ist die CIL-Anweisung, die aufrufende Schnittstellenmethoden verarbeitet, wenn Sie mehr darüber erfahren möchten , wie die Laufzeitumgebung dieses Setup verarbeitet.call
Opcode fürstatic
Methoden verwendet wird, interessanterweisecallvirt
auch, wenn die Klasse verwendet wirdsealed
.Wenn mit "Elternklasse" "Basisklasse" gemeint ist, dann ist dies in gcc nicht der Fall (was ich auch in keinem anderen Compiler erwarte).
Wenn C von B abgeleitet ist und A eine polymorphe Klasse ist, hat die C-Instanz genau eine vtable.
Der Compiler verfügt über alle Informationen, die zum Zusammenführen der Daten in der V-Tabelle von A mit den Daten von B und von B mit den Daten von C erforderlich sind.
Hier ist ein Beispiel: https://godbolt.org/g/sfdtNh
Sie werden sehen, dass es nur eine Initialisierung einer vtable gibt.
Ich habe die Assembly-Ausgabe für die Hauptfunktion hier mit Anmerkungen kopiert:
Komplette Quelle als Referenz:
quelle
class Derived : public FirstBase, public SecondBase
dann kann es zwei vtables geben. Sie können ausführeng++ -fdump-class-hierarchy
, um das Klassenlayout zu sehen (auch in meinem verknüpften Blog-Beitrag gezeigt). Godbolt zeigt dann vor dem Aufruf ein zusätzliches Zeigerinkrement an, um die 2. V-Tabelle auszuwählen.