Kann mir jemand im Detail erklären, wie genau die virtuelle Tabelle funktioniert und welche Zeiger beim Aufruf von virtuellen Funktionen zugeordnet sind.
Wenn sie tatsächlich langsamer sind, können Sie dann anzeigen, dass die Ausführung der virtuellen Funktion mehr Zeit in Anspruch nimmt als normale Klassenmethoden? Es ist leicht, den Überblick zu verlieren, wie / was passiert, ohne Code zu sehen.
Antworten:
Virtuelle Methoden werden üblicherweise über sogenannte virtuelle Methodentabellen (kurz vtable) implementiert, in denen Funktionszeiger gespeichert sind. Dies fügt dem eigentlichen Aufruf eine Indirektion hinzu (muss die Adresse der aufzurufenden Funktion aus der vtable abrufen und dann aufrufen - im Gegensatz zum bloßen Aufrufen im Voraus). Natürlich dauert dies einige Zeit und etwas mehr Code.
Dies ist jedoch nicht unbedingt die Hauptursache für Langsamkeit. Das eigentliche Problem ist, dass der Compiler (in der Regel) nicht wissen kann, welche Funktion aufgerufen wird. Daher kann es keine Inline-Optimierung durchführen. Dies allein könnte ein Dutzend sinnloser Anweisungen hinzufügen (Register vorbereiten, aufrufen und anschließend den Zustand wiederherstellen) und andere, scheinbar nicht zusammenhängende Optimierungen verhindern. Wenn Sie wie verrückt verzweigen, indem Sie viele verschiedene Implementierungen aufrufen, erleiden Sie dieselben Hits, die Sie wie verrückt verzweigen würden: Der Cache und der Verzweigungsprädiktor helfen Ihnen nicht, die Verzweigungen dauern länger als perfekt vorhersehbar Ast.
Groß aber : Diese Leistungstreffer sind normalerweise zu klein, um von Bedeutung zu sein. Sie sollten überlegen, ob Sie einen Hochleistungscode erstellen und eine virtuelle Funktion hinzufügen möchten, die mit alarmierender Häufigkeit aufgerufen wird. Doch auch bedenken , dass Anrufe virtuelle Funktion mit anderen Mitteln der Verzweigung (ersetzen
if .. else
,switch
, Funktionszeiger, etc.) werden nicht das grundlegende Problem lösen - es ist sehr gut langsamer sein kann. Das Problem (falls überhaupt vorhanden) sind keine virtuellen Funktionen, sondern eine (unnötige) Indirektion.Bearbeiten: Der Unterschied in den Anrufanweisungen wird in anderen Antworten beschrieben. Grundsätzlich lautet der Code für einen statischen ("normalen") Aufruf:
Ein virtueller Aufruf macht genau dasselbe, außer dass die Funktionsadresse zur Kompilierungszeit nicht bekannt ist. Stattdessen ein paar Anweisungen ...
Was Zweige betrifft: Ein Zweig ist alles, was zu einem anderen Befehl springt, anstatt nur den nächsten Befehl ausführen zu lassen. Dazu gehören
if
,switch
Teile von verschiedenen Schleifen, Funktionsaufrufe, etc. , und manchmal sind die Compiler implementiert Dinge , die nicht in einer Art und Weise zu tun Zweig scheint, braucht eigentlich einen Zweig unter der Haube. Siehe Warum verarbeitet ein sortiertes Array schneller als ein unsortiertes Array? Warum das so langsam sein kann, was CPUs tun, um dieser Verlangsamung entgegenzuwirken, und warum dies kein Allheilmittel ist.quelle
virtual
.Hier ist ein Teil des tatsächlichen disassemblierten Codes eines virtuellen Funktionsaufrufs bzw. eines nicht virtuellen Aufrufs:
Sie können sehen, dass für den virtuellen Anruf drei zusätzliche Anweisungen erforderlich sind, um die richtige Adresse zu ermitteln, während die Adresse des nicht virtuellen Anrufs in kompiliert werden kann.
Beachten Sie jedoch, dass diese zusätzliche Nachschlagezeit in den meisten Fällen als vernachlässigbar angesehen werden kann. In Situationen, in denen die Nachschlagezeit signifikant ist, wie in einer Schleife, kann der Wert normalerweise zwischengespeichert werden, indem die ersten drei Anweisungen vor der Schleife ausgeführt werden.
Die andere Situation, in der die Nachschlagezeit erheblich wird, besteht darin, dass Sie über eine Sammlung von Objekten verfügen und in einer Schleife eine virtuelle Funktion für jedes dieser Objekte aufrufen. In diesem Fall benötigen Sie jedoch einige Mittel, um die aufzurufende Funktion auszuwählen, und eine virtuelle Tabellensuche ist ein ebenso gutes Mittel wie jedes andere . Da der vtable-Lookup-Code so häufig verwendet wird, dass er stark optimiert ist, kann der Versuch, ihn manuell zu umgehen, mit hoher Wahrscheinlichkeit zu einer schlechteren Leistung führen.
quelle
-0x8(%rbp)
. Oh mein Gott ... diese AT & T-Syntax.Langsamer als was ?
Virtuelle Funktionen lösen ein Problem, das nicht durch direkte Funktionsaufrufe gelöst werden kann. Im Allgemeinen können Sie nur zwei Programme vergleichen, die dasselbe berechnen. "Dieser Raytracer ist schneller als dieser Compiler" ist nicht sinnvoll, und dieses Prinzip lässt sich sogar auf kleine Dinge wie einzelne Funktionen oder Programmiersprachenkonstrukte verallgemeinern.
Wenn Sie keine virtuelle Funktion verwenden, um dynamisch zu einem Code zu wechseln, der auf einem Datum basiert, z. B. dem Typ eines Objekts, müssen Sie etwas anderes verwenden, z. B. eine
switch
Anweisung, um dasselbe zu erreichen. Dieses etwas andere hat seine eigenen Gemeinkosten und Auswirkungen auf die Organisation des Programms, die seine Wartbarkeit und globale Leistung beeinflussen.Beachten Sie, dass Aufrufe an virtuelle Funktionen in C ++ nicht immer dynamisch sind. Wenn ein Objekt aufgerufen wird, dessen genauer Typ bekannt ist (weil das Objekt kein Zeiger oder Verweis ist oder weil sein Typ ansonsten statisch abgeleitet werden kann), handelt es sich bei den Aufrufen nur um reguläre Memberfunktionsaufrufe. Dies bedeutet nicht nur, dass keine zusätzlichen Kosten für den Versand anfallen, sondern dass diese Anrufe auf die gleiche Weise wie normale Anrufe eingebunden werden können.
Mit anderen Worten, Ihr C ++ - Compiler kann feststellen, ob für virtuelle Funktionen kein virtueller Versand erforderlich ist. Daher gibt es normalerweise keinen Grund, sich um die Leistung im Vergleich zu nicht virtuellen Funktionen Sorgen zu machen.
Neu: Auch Shared Libraries dürfen nicht vergessen werden. Wenn Sie eine Klasse verwenden, die sich in einer gemeinsam genutzten Bibliothek befindet, ist der Aufruf einer normalen Member-Funktion nicht einfach eine nette Befehlssequenz
callq 0x4007aa
. Es muss ein paar Schritte durchlaufen, wie das Indirektisieren durch eine "Programmverknüpfungstabelle" oder eine solche Struktur. Daher könnte die Indirektion einer gemeinsam genutzten Bibliothek den Kostenunterschied zwischen einem (wirklich indirekten) virtuellen Anruf und einem direkten Anruf etwas (wenn nicht vollständig) ausgleichen. Bei Überlegungen zu Kompromissen bei virtuellen Funktionen muss daher berücksichtigt werden, wie das Programm aufgebaut ist: Ist die Klasse des Zielobjekts monolithisch mit dem Programm verknüpft, das den Aufruf ausführt?quelle
weil ein virtueller Anruf gleichbedeutend ist mit
Wenn der Compiler mit einer nicht-virtuellen Funktion die erste Zeile konstant falten kann, ist dies eine Dereferenzierung, eine Hinzufügung und ein dynamischer Aufruf, der nur in einen statischen Aufruf umgewandelt wird
Dadurch kann auch die Funktion eingebunden werden (mit allen Konsequenzen für die Optimierung).
quelle