Meine Fragen sind im Wesentlichen vollständig im Titel angegeben, lassen Sie mich jedoch näher darauf eingehen.
Frage:
Vielleicht neu zu formulieren, wie kompliziert / einfach muss die virtual
-Methode sein, um den Mechanismus zu einem erheblichen Overhead zu machen? Gibt es dafür Faustregeln? Z.B. Wenn es 10 Minuten dauert, verwendet I / O, komplexe if
-Anweisungen, Speicheroperationen usw. ist es kein Problem. Oder, wenn Sie virtual get_r() { return sqrt( x*x + y*y); };
schreiben und es in einer Schleife aufrufen, haben Sie Probleme.
Ich hoffe, dass die Frage nicht zu allgemein ist, da ich einige allgemeine, aber konkrete technische Antworten suche. Entweder ist es schwer / unmöglich zu sagen, oder virtuelle Aufrufe benötigen so viel Zeit / Zyklen Ressourcen, und Mathematik nimmt dies, I / O dies.
Vielleicht kennen einige Techniker einige allgemeine Zahlen, um sie zu vergleichen oder eine Analyse durchzuführen, und sie können allgemeine Schlussfolgerungen teilen. Peinlicherweise weiß ich nicht, wie man diese schicke asm
analyse = / macht.
Ich möchte auch einige Gründe dafür nennen, ebenso wie meinen Anwendungsfall.
Ich denke, ich habe mehr als nur ein paar Fragen gesehen, dass Leute während der Dürre keine virtuellen Maschinen wie offenes Feuer im Wald benutzen, um der Leistung willen und so viele Leute fragen: "Sind Sie absolut sicher, dass der virtuelle Overhead wirklich ein Problem ist? in deinem Fall? ".
In meiner jüngsten Arbeit stieß ich auf ein Problem, das auf beiden Seiten des Flusses liegen kann, glaube ich.
Bedenken Sie auch, ich frage nicht, wie man die Implementierung der Schnittstelle verbessern kann. Ich glaube, ich weiß, wie es geht. Ich frage, ob es möglich ist, zu sagen, wann es zu tun ist, oder welche rechts von der Fledermaus zu wählen.
Anwendungsfall:
Ich mache ein paar Simulationen. Ich habe eine Klasse, die im Grunde eine Laufumgebung bietet. Es gibt eine Basisklasse und mehr als eine abgeleitete Klasse, die verschiedene Workflows definieren. Base sammelt Daten als allgemeine Logik und weist I / O-Quellen und Senken zu. Derivate definieren bestimmte Workflows mehr oder weniger, indem sie RunEnv::run()
implementieren. Ich denke, das ist ein gültiges Design. Nun stellen wir uns vor, Objekte, die Gegenstand des Workflows sind, können in 2D- oder 3D-Ebene platziert werden. Die Workflows sind in beiden Fällen gemeinsam / austauschbar, so dass die Objekte, an denen wir arbeiten, eine gemeinsame Schnittstelle haben können, allerdings zu sehr einfachen Methoden wie Object::get_r()
. Darüber hinaus können einige Stat-Logger für die Umgebung definiert werden.
Ursprünglich wollte ich ein paar Code-Snippets bereitstellen, aber es endete mit 5 Klassen und 2-4 Methoden, also jeder Wand von code
. Ich kann es auf Anfrage posten, aber es würde die Frage auf das Doppelte der aktuellen Größe verlängern.
Die wichtigsten Punkte sind: RunEnv::run()
ist die Hauptschleife. Normalerweise sehr lang (5 Minuten bis 5 Stunden). Es bietet grundlegende Zeitinstrumentierung, Aufrufe RunEnv::process_iteration()
und RunEnv::log_stats()
. Alle sind virtuell. Begründung ist. Ich kann die RunEnv
ableiten, die run()
beispielsweise für verschiedene Stoppbedingungen neu gestalten. Ich kann process_iteration()
neu gestalten, um beispielsweise Multi-Threading zu verwenden, wenn ich einen Pool von Objekten verarbeiten und sie auf verschiedene Arten verarbeiten muss. Auch verschiedene Workflows möchten verschiedene Statistiken protokollieren. RunEnv::log_stats()
ist nur ein Aufruf, der bereits berechnete interessante Statistiken in std::ostream
ausgibt. Ich rate unter Verwendung von virtuals und hat keinen wirklichen Einfluss.
Nehmen wir an, die Iteration funktioniert, indem die Entfernung von Objekten zum Ursprung berechnet wird. Also haben wir als Schnittstelle double Obj::get_r();
. Obj
sind Implementierungen für 2D- und 3D-Fälle. Der Getter ist in beiden Fällen eine einfache Mathematik mit 2-3 Multiplikationen und Additionen.
Ich experimentierte auch mit verschiedenen Speicherfunktionen. Z.B. manchmal wurden Koordinatendaten in privaten Variablen und manchmal in einem gemeinsam genutzten Pool gespeichert, so dass sogar die get_x()
mit der Implementierung get_x(){return x;};
oder get_x(){ return pool[my_num*dim+x_offset]; };
virtuell gemacht werden konnte. Stellen Sie sich vor, Sie würden etwas mit get_r(){ sqrt(get_x()*get_x() + get_y()*get_y()) ;};
berechnen. Ich vermute, dass Virtualität hier die Performance zerstören würde.
Der Aufruf virtueller Methoden in C ++ auf einem x86-Code ergibt den folgenden Code (einfache Vererbung):
%Vor% Ohne virtuell werden Sie eine Anweisung mov
im Vergleich zu einer nicht virtuellen Elementfunktion ersparen. Im Falle einer einzelnen Vererbung ist der Leistungshit vernachlässigbar.
Falls Sie eine mehrfache Vererbung oder, noch schlimmer, virtuelle Vererbung haben, können die virtuellen Aufrufe sehr viel komplexer sein. Aber das ist mehr Problem der Klassenhierarchie und Architektur.
Die Faustregel:
Wenn der Körper der Methode viele Male (& gt; 100x) langsamer ist als eine einzelne mov
-Anweisung - benutze einfach virtual
und störe nicht. Ansonsten - Profiliere deine Engpässe und optimiere sie.
Aktualisierung:
Für Fälle mit mehreren / virtuellen Vererbungen lesen Sie diese Seite: Ссылка
Gibt es dafür Faustregeln?
Die beste, allgemeingültige Faustregel für Fragen wie diese ist:
messen Sie Ihren Code, bevor Sie ihn optimieren
Wenn Sie versuchen, Ihren Code ohne Messungen zu perfektionieren, ist dies ein sicherer Weg zu unnötig komplexem Code, der an allen falschen Stellen optimiert wurde.
Machen Sie sich also keine Gedanken über den Overhead einer virtuellen Funktion, bis Sie den Beweis erbracht haben, dass virtual
das Problem ist. Wenn Sie solche Beweise haben, dann können Sie arbeiten, um die virtual
in diesem Fall zu entfernen. Wahrscheinlicher ist jedoch, dass Sie Wege finden, Ihre Berechnungen zu beschleunigen oder zu vermeiden, dass Sie berechnen, wo Sie dies nicht benötigen. Dadurch werden wesentlich größere Leistungsverbesserungen erzielt. Aber wieder, raten Sie nicht - messen Sie zuerst.
Zunächst hängt natürlich jeder Unterschied vom Compiler ab, die Architektur usw. Auf einigen Maschinen ist der Unterschied zwischen ein virtueller Anruf und ein nicht virtueller Anruf werden kaum messbar sein, auf mindestens einem anderen, es wird (oder würde --- meine Erfahrung mit Diese Maschine ist ziemlich alt) vollständig reinigen Sie die Pipeline (keine Verzweigungsvorhersage für indirekte Sprünge).
Bei den meisten Prozessoren ist der Verlust der Inline-Fähigkeit der eigentliche Nachteil virtueller Funktionen. mit dem daraus resultierenden Verlust anderer Optimierungsmöglichkeiten. Mit anderen Worten, die Kosten wird tatsächlich von dem Kontext abhängen, in dem die Funktion aufgerufen wird.
Mehr auf den Punkt: virtuelle Funktionen und nicht-virtuell Funktionen haben unterschiedliche Semantiken. Sie können also nicht wählen: wenn Sie brauche virtuelle Semantik, musst du virtuell benutzen; wenn du es nicht tust benötigen virtuelle Semantik, können Sie nicht virtuell verwenden. Also die Frage kommt wirklich nicht hoch.
Die absolut grundlegendste Empfehlung, die wiederum, wie andere gesagt haben, etwas ist, das Sie in Ihrer spezifischen Anwendung und Umgebung profilieren sollten, ist virtual
in engen Schleifen zu vermeiden.
Beachten Sie, dass virtuelle Memberfunktionen sich wahrscheinlich besser verhalten als die meisten Alternativen, wenn Sie polymorphes Verhalten benötigen. Die Ausnahme kann sein, wenn Sie eine Sammlung von polymorphen, aber homogenen Typen haben (die Sammlung könnte eine beliebige der polymorphen Typen sein, aber sie sind alle vom selben Typ, egal welcher Typ sie sind). Sie sind dann besser dran, wenn Sie das polymorphe Verhalten außerhalb der Schleife verschieben.
Wenn Sie das klassische dumme Bad-OO-Beispiel mit Formen verwenden, sind Sie besser dran mit:
%Vor%Als die naive Version, die etwa so aussehen könnte:
%Vor% Dies funktioniert wiederum nur mit homogenen Typen in Sammlungen. Wenn Ihr Collection
sowohl Rectangle
s als auch Circle
s enthalten könnte, müssen Sie bei jeder Iteration für jede Instanz unterscheiden, welche Zeichenmethode verwendet werden soll. Eine virtuelle Funktion wird wahrscheinlich schneller sein als ein Funktionszeiger oder eine switch-Anweisung (aber Profil, um sicher zu sein).
Das Ziel mit dem obigen Code war, das polymorphe Verhalten aus der Schleife zu entfernen. Dies ist nicht immer möglich, aber wenn dies der Fall ist, wird es normalerweise zu einer gewissen Leistung führen. Bei einer großen Anzahl von Objekten (z. B. einem Partikelsimulator) könnte der Leistungsunterschied ziemlich auffallen.
Wenn eine Funktion nicht viele tausend Male innerhalb einer Schleife aufgerufen wird, werden Sie wahrscheinlich keinen messbaren Unterschied zwischen virtuellen und nicht-virtuellen Funktionen bemerken. Aber profile es zum Testen und sei sicher, wenn du denkst, dass es wichtig ist.
Tags und Links c++ performance