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:
- Kopieren Sie einige Register auf dem Stapel, damit die aufgerufene Funktion diese Register verwenden kann.
- Kopieren Sie die Argumente an vordefinierte Positionen, damit die aufgerufene Funktion sie unabhängig von ihrem Aufruf finden kann.
- Schieben Sie die Absenderadresse.
- Verzweigen / Springen zum Funktionscode, der eine Adresse zur Kompilierungszeit ist und daher vom Compiler / Linker in der Binärdatei fest codiert wird.
- Rufen Sie den Rückgabewert von einem vordefinierten Speicherort ab und stellen Sie die zu verwendenden Register wieder her.
Ein virtueller Aufruf macht genau dasselbe, außer dass die Funktionsadresse zur Kompilierungszeit nicht bekannt ist. Stattdessen ein paar Anweisungen ...
- Rufen Sie den vtable-Zeiger vom Objekt ab, der auf ein Array von Funktionszeigern (Funktionsadressen) zeigt, einen für jede virtuelle Funktion.
- Holen Sie sich die richtige Funktionsadresse aus der vtable in ein Register (der Index, in dem die richtige Funktionsadresse gespeichert ist, wird zur Kompilierungszeit festgelegt).
- Springen Sie zu der Adresse in diesem Register, anstatt zu einer fest codierten Adresse zu springen.
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.