virtuelle Funktion vs. Funktionszeiger - Leistung?

8

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.

    
user1054922 30.07.2013, 23:36
quelle

5 Antworten

19

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.

    
Antti Haapala 30.07.2013, 23:42
quelle
10
  

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.

    
Team Upvote 01.12.2015 09:29
quelle
6

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.

    
Mats Petersson 30.07.2013 23:42
quelle
5

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.

    
EJP 30.07.2013 23:42
quelle
2

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:

  • Eine virtuelle Funktion hat eine Speicherlesung für die vtable und den indirekten Aufruf an die Adresse der Funktion
  • Ein Funktionszeiger hat nur einen indirekten Aufruf an die Funktion

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.

    
Jack 30.07.2013 23:43
quelle

Tags und Links