Ich erinnere mich, irgendwo gelesen zu haben, dass die Kosten eines virtuellen Anrufs in C # relativ gesehen nicht so hoch sind wie in C ++. Ist das wahr? Wenn ja - warum?
Ein virtueller C # -Aufruf muss prüfen, ob "this" null ist und ein virtueller C ++ - Aufruf nicht. So kann ich generell nicht sehen, warum ein C # virtueller Anruf schneller wäre. In speziellen Fällen kann der C # -Compiler (oder JIT-Compiler) in der Lage sein, den virtuellen Aufruf besser zu verketten als ein C ++ - Compiler, da ein C # -Compiler Zugriff auf bessere Typinformationen hat. Die Aufrufmethodenanweisung kann manchmal in C ++ langsamer sein, da die C # JIT möglicherweise eine schnellere Anweisung verwenden kann, die nur mit einem kleinen Offset zurechtkommt, da sie mehr über das Laufzeitspeicherlayout und das Prozessormodell weiß ein C ++ - Compiler.
Allerdings sprechen wir hier nur von einer Handvoll Prozessor-Anweisungen. Bei einem Modem-Superscalar-Prozessor ist es sehr gut möglich, dass der Befehl "Null-Überprüfung" gleichzeitig mit der "Aufruf-Methode" ausgeführt wird und daher keine Zeit benötigt.
Es ist auch sehr wahrscheinlich, dass alle Prozessorbefehle bereits im Level 1 Cache sind, wenn der Aufruf in einer Schleife erfolgt. Es ist jedoch weniger wahrscheinlich, dass die Daten Caches sind. Die Kosten für das Lesen eines Datenwerts aus dem Hauptspeicher sind heutzutage dieselben wie das Ausführen von 100s von Befehlen aus dem Cache der Ebene 1. Daher ist es unglücklich, dass in realen Anwendungen die Kosten eines virtuellen Anrufs sogar an mehr als nur wenigen Stellen messbar sind.
Die Tatsache, dass der C # -Code einige weitere Anweisungen verwendet, wird natürlich die Menge an Code reduzieren, die in den Cache passen kann, deren Auswirkung unmöglich vorherzusagen ist.
(Wenn die C ++ - Klasse mehrere Inhärenzwerte verwendet, sind die Kosten höher, da der "this" -Zeiger aktualisiert werden muss. Schnittstellen in C # fügen eine weitere Umleitungsebene hinzu.)
Für JIT-kompilierte Sprachen (ich weiß nicht, ob CLR dies tut oder nicht, Suns JVM tut es), ist es eine allgemeine Optimierung, einen virtuellen Aufruf, der nur zwei oder drei Implementierungen hat, in eine Sequenz von Tests auf dem Typ und umzuwandeln direkte oder Inline-Anrufe.
Der Vorteil besteht darin, dass moderne Pipeline-CPUs Verzweigungsvorhersage und Vorabruf von Direktaufrufen verwenden können, aber ein indirekter Aufruf (dargestellt durch einen Funktionszeiger in Hochsprachen) führt häufig zum Stillstand der Pipeline.
Im Grenzfall, wo es nur eine Implementierung des virtuellen Aufrufs gibt und der Rumpf des Aufrufs klein genug ist, reduziert sich der virtuelle Aufruf auf Inline-Code . Diese Technik wurde in der Runtime der Selbstsprache verwendet, aus der sich die JVM entwickelt hat.
Die meisten C ++ - Compiler führen nicht die gesamte Programmanalyse durch, die zur Durchführung dieser Optimierung erforderlich ist, aber Projekte wie LLVM betrachten ganze Programmoptimierungen wie diese.
Die ursprüngliche Frage lautet:
Ich erinnere mich, irgendwo gelesen zu haben dass die Kosten eines virtuellen Anrufs in C # ist nicht so hoch, relativ Sprechen , wie in C ++.
Beachten Sie die Betonung. Mit anderen Worten, die Frage könnte wie folgt umformuliert werden:
Ich erinnere mich, irgendwo gelesen zu haben das in C #, virtuell und nicht virtuell Aufrufe sind gleichermaßen langsam, während in C ++ Ein virtueller Anruf ist langsamer als a nicht virtueller Anruf ...
Der Fragesteller behauptet also nicht, dass C # unter keinen Umständen schneller ist als C ++.
Vielleicht eine nutzlose Ablenkung, aber das hat meine Neugier auf C ++ mit / clr: pure geweckt, ohne C ++ / CLI-Erweiterungen. Der Compiler erzeugt IL, das vom JIT in nativen Code konvertiert wird, obwohl es reines C ++ ist. Hier haben wir eine Möglichkeit zu sehen, was eine Standard-C ++ - Implementierung macht, wenn sie auf der gleichen Plattform wie C # läuft.
Mit einer nicht virtuellen Methode:
%Vor%Dieser Code:
%Vor% ... bewirkt, dass der Befehlscode call
mit dem spezifischen Methodennamen ausgegeben wird, wobei Bar ein implizites this
Argument übergeben wird.
Vergleiche mit einer Vererbungshierarchie:
%Vor%Wenn wir es jetzt tun:
%Vor% Das gibt stattdessen den calli
Opcode aus, der zu einer berechneten Adresse springt - es gibt also viel IL vor dem Aufruf. Indem wir es in C # zurückdrehen, können wir sehen, was vor sich geht:
Mit anderen Worten: Wirf die Adresse von b
auf einen Zeiger auf int (der zufällig die gleiche Größe wie ein Zeiger hat) und nimm den Wert an dieser Stelle, der Adresse der vtable, und nimm dann an das erste Element in der vtable, welches die Adresse ist, zu der gesprungen werden soll, es dereferenzieren und es aufrufen, wobei es das implizite Argument this
übergeben wird.
Wir können das virtuelle Beispiel optimieren, um C ++ / CLI-Erweiterungen zu verwenden:
%Vor% Dies erzeugt den callvirt
Opcode genau wie in C #:
Beim Kompilieren zum Targeting der CLR verfügt der aktuelle C ++ - Compiler von Microsoft nicht über die gleichen Optimierungsmöglichkeiten wie C #, wenn die Standardfunktionen der einzelnen Sprachen verwendet werden. Für eine Standard-C ++ - Klassenhierarchie generiert der C ++ - Compiler Code, der fest codierte Logik zum Durchlaufen der V-Tabelle enthält, während er für eine ref-Klasse dem JIT überlassen bleibt, um die optimale Implementierung herauszufinden.
Ich nehme an, dass diese Annahme auf JIT-Compiler basiert, was bedeutet, dass C # wahrscheinlich einen virtuellen Aufruf in einen einfachen Methodenaufruf konvertiert, bevor er tatsächlich verwendet wird.
Aber es ist im Wesentlichen theoretisch und ich würde nicht darauf wetten!
Die Kosten eines virtuellen Aufrufs in C ++ sind die eines Funktionsaufrufs über einen Zeiger (vtbl). Ich bezweifle, dass C # das schneller machen kann und trotzdem den Objekttyp zur Laufzeit bestimmen kann ...
Bearbeiten: Wie Pete Kirkham darauf hinwies, könnte ein guter JIT den C # -Aufruf inline machen und einen Pipeline-Stillstand vermeiden. etwas, was die meisten C ++ - Compiler (noch) nicht können. Auf der anderen Seite erwähnte Ian Ringrose die Auswirkungen auf die Cache-Nutzung. Hinzu kommt, dass das JIT selbst läuft, und (streng persönlich) würde ich mich nicht wirklich darum kümmern, es sei denn, das Profilieren auf dem Zielrechner unter realistischen Arbeitslasten hat sich als schneller als der andere erwiesen. Es ist bestenfalls Mikrooptimierung.
Nicht sicher über das vollständige Framework, aber im Compact Framework wird es langsamer sein, weil CF keine virtuellen Aufruftabellen hat, obwohl es das Ergebnis zwischenspeichert. Dies bedeutet, dass ein virtueller Aufruf in CF beim ersten Aufruf langsamer ist, da er eine manuelle Suche durchführen muss. Es kann jedes Mal langsam sein, wenn es aufgerufen wird, wenn die App wenig Speicher hat, da die zwischengespeicherte Suche aufgeschlagen werden kann.
C # flacht die Vtable- und Inline-Aufrufe von Vorfahren ab, sodass Sie die Vererbungshierarchie nicht verketten, um irgendetwas aufzulösen.
Es ist möglicherweise nicht genau die Antwort auf Ihre Frage, aber obwohl .NET JIT die virtuellen Aufrufe optimiert, wie alle zuvor sagten, profilgesteuerte Optimierung in Visual Studio 2005 und 2008 führt virtuelle Spekulationen durch Einfügen eines direkten Aufrufs an die wahrscheinlichste Zielfunktion durch, indem der Aufruf umrahmt wird, sodass das Gewicht gleich sein kann .
Tags und Links c# c++ virtual polymorphism