Werden virtuelle Funktionen von C ++ für eine polymorphe Basisklasse genauso schnell aufgerufen wie ein C-Style-Funktionszeiger? Gibt es wirklich einen Unterschied?
Ich überlege, einen leistungsorientierten Code zu refactorieren, der Funktionszeiger verwendet und sie in Polymorphismus in virtuelle Funktionen umwandelt.
Ich würde sagen, die meisten C ++ - Implementierungen funktionieren ähnlich (und wahrscheinlich die erste) Implementierungen, die in C kompiliert wurden, erzeugten Code wie diesen):
%Vor% Dann wird bei einer Instanz Class x
die Methode virtualmethod1
aufgerufen, wie x.vtable->virtualmethod1(&x)
, also eine zusätzliche Dereferenzierung, 1 indizierte Suche aus vtable
und ein zusätzliches Argument (= this
) auf den Stapel geschoben.
Allerdings kann der Compiler wahrscheinlich wiederholte Methodenaufrufe für eine Instanz innerhalb einer Funktion optimieren: Da eine Instanz Class x
ihre Klasse nicht ändern kann, nachdem sie konstruiert wurde, kann der Compiler die gesamte x.vtable->virtualmethod1
als gemeinsamen Unterausdruck betrachten, und verschiebe es aus Schleifen. In diesem Fall würden also die wiederholten virtuellen Methodenaufrufe innerhalb einer einzigen Funktion dem Aufruf einer Funktion über einen einfachen Funktionszeiger entsprechen.
Werden virtuelle Funktionen von C ++ für eine polymorphe Basisklasse aufgerufen? schnell wie einen C-Style-Funktionszeiger aufrufen? Gibt es wirklich welche? Unterschied?
Äpfel und Orangen. Auf einer winzigen "Eins-gegen-Eins" -Level-Ebene erfordert ein virtueller Funktionsaufruf etwas mehr Arbeit, da ein Indirektions- / Indexierungsaufwand von vptr
auf vtable
entry kommt.
Aber ein virtueller Funktionsaufruf kann schneller sein
Okay, wie könnte das sein? Ich habe gerade gesagt, ein virtueller Funktionsaufruf erfordert etwas mehr Arbeit, was stimmt.
Was die Leute vergessen, ist, hier einen Vergleich zu machen (um zu versuchen, es ein bisschen weniger Äpfel und Orangen zu machen, obwohl es Äpfel und Orangen sind). Normalerweise erstellen wir keine Klasse mit nur einer virtuellen Funktion. Wenn wir das täten, würde die Performance (und sogar Dinge wie die Codegröße) definitiv einen Funktionszeiger bevorzugen. Wir haben oft etwas mehr so:
%Vor%... in diesem Fall könnte eine "direktere" Funktion Zeiger-Analogie sein:
%Vor% In diesem Fall kann das Aufrufen virtueller Funktionen in jeder Instanz von Foo
erheblich effizienter sein als Bar
. Dies liegt daran, dass Foo
nur ein einziges vptr in einer zentralen vtable speichern muss, auf die wiederholt zugegriffen wird. Damit erhalten wir eine verbesserte Referenzlokalität (kleiner Foos
und solche, die potentiell besser passen und in einer Cachezeile, häufigerer Zugriff auf Foo's
zentrale vtable).
Bar
andererseits benötigt mehr Speicher und dupliziert effektiv den Inhalt von Foo's
vtable in jeder Instanz von Bar
(angenommen, es gibt eine Million Instanzen von Foo
und Bar
) . In diesem Fall wird die Menge der redundanten Daten, die die Größe von Bar
aufblähen, oft die Kosten von etwas weniger Arbeit pro Funktionszeiger-Aufruf deutlich überwiegen.
Wenn wir nur einen Funktionszeiger pro Objekt speichern müssen, und dies war ein extremer Hotspot, dann kann es gut sein, einfach einen Funktionszeiger zu speichern (zB: er könnte nützlich sein, wenn jemand etwas remote wie std::function
implementiert) einfach einen Funktionszeiger speichern).
Es ist also eine Art Äpfel und Orangen, aber wenn wir einen Anwendungsfall so nah wie möglich modellieren, kann der vtable-Ansatz, der eine zentrale gemeinsame Tabelle von Funktionsadressen (in C oder C ++) speichert, wesentlich mehr sein effizient.
Wenn wir einen Anwendungsfall modellieren, in dem wir nur einen einzelnen Funktionszeiger in einem Objekt gespeichert haben, im Gegensatz zu einer vtable, die nur eine virtuelle Funktion enthält, wäre der Funktionszeiger etwas effizienter.
Es ist UNGLÜCKLICH, dass Sie einen großen Unterschied sehen werden, aber wie bei all diesen Dingen sind es oft die kleinen Details (wie der Compiler, der einen this
-Zeiger an eine virtuelle Funktion übergeben muss), die Unterschiede in der Leistung verursachen können . Die virtual
-Funktion selbst ist ein Funktionszeiger "unter der Haube", so dass Sie wahrscheinlich in beiden Fällen ziemlich ähnlichen Code bekommen, sobald der Compiler seine Sache gemacht hat.
Es klingt nach einer guten Nutzung virtueller Funktionen, und wenn jemand widersprach und sagte "es wird einen Leistungsunterschied geben", würde ich sagen "beweise es". Aber wenn Sie diese Diskussion vermeiden möchten, erstellen Sie einen Benchmark (falls es noch keinen gibt), der die Leistung des vorhandenen Codes misst, refaktorieren Sie ihn (oder einen Teil davon) und vergleichen Sie die Ergebnisse. Idealerweise sollten Sie einige verschiedene Maschinen testen, damit Sie keine Ergebnisse erzielen, die auf Ihrer Maschine besser funktionieren, aber auf einigen anderen Maschinentypen (verschiedene Generationen von Prozessoren, unterschiedlicher Hersteller oder Prozessor usw.) nicht so gut.
Ein virtueller Funktionsaufruf beinhaltet zwei Dereferenzen, von denen eine indiziert ist, d.h. etwas wie *(object->_vtable[3])()
.
Ein Aufruf über einen Funktionszeiger beinhaltet eine Dereferenzierung.
Für einen Methodenaufruf muss außerdem ein verborgenes Argument übergeben werden, das als this
empfangen wird.
Wenn der Methodenkörper praktisch leer ist und es keine Argumente oder Rückgabewerte gibt, ist es unwahrscheinlich, dass Sie den Unterschied bemerken.
Der Unterschied zwischen einem Function-Pointer-Aufruf und einem virtuellen Funktionsaufruf ist vernachlässigbar, es sei denn, Sie haben bereits festgestellt, dass dies ein Flaschenhals ist.
Der einzige Unterschied ist:
Dies ist der Fall, weil die virtuelle Funktion verlangt, die Adresse der Funktion zu suchen, die sie aufruft, während der Funktionszeiger sie bereits kennt (da sie in sich selbst gespeichert ist).
Ich würde hinzufügen, dass, da Sie mit C ++ arbeiten, virtuelle Methoden der richtige Weg sein sollten.