Ich bin auf viele Optimierungstipps gestoßen, die besagen, dass Sie Ihre Klassen als versiegelt markieren sollten, um zusätzliche Leistungsvorteile zu erzielen.
Ich habe einige Tests durchgeführt, um den Leistungsunterschied zu überprüfen, und keine gefunden. Mache ich etwas falsch? Vermisse ich den Fall, dass versiegelte Klassen bessere Ergebnisse liefern?
Hat jemand Tests durchgeführt und einen Unterschied festgestellt?
Hilf mir zu lernen :)
.net
optimization
frameworks
performance
Vaibhav
quelle
quelle
Antworten:
Der JITter verwendet manchmal nicht virtuelle Aufrufe von Methoden in versiegelten Klassen, da sie nicht weiter erweitert werden können.
Es gibt komplexe Regeln für den Aufruftyp "virtuell / nicht virtuell", und ich kenne sie nicht alle, sodass ich sie nicht wirklich für Sie skizzieren kann. Wenn Sie jedoch nach versiegelten Klassen und virtuellen Methoden googeln, finden Sie möglicherweise einige Artikel zu diesem Thema.
Beachten Sie, dass jede Art von Leistungsvorteil, die Sie durch diese Optimierungsstufe erhalten würden, als letzter Ausweg betrachtet werden sollte. Optimieren Sie immer auf algorithmischer Ebene, bevor Sie auf Codeebene optimieren.
Hier ist ein Link, der dies erwähnt: Durchstreifen des versiegelten Schlüsselworts
quelle
Die Antwort lautet: Nein, versiegelte Klassen schneiden nicht besser ab als nicht versiegelte.
Das Problem liegt in den
call
vscallvirt
IL-Operationscodes.Call
ist schneller alscallvirt
undcallvirt
wird hauptsächlich verwendet, wenn Sie nicht wissen, ob das Objekt in eine Unterklasse unterteilt wurde. Die Leute gehen also davon aus, dass sich alle Op-Codes voncalvirts
zu änderncalls
und schneller sind , wenn Sie eine Klasse versiegeln .Leider
callvirt
macht es auch andere Dinge, die es nützlich machen, wie das Suchen nach Nullreferenzen. Dies bedeutet, dass selbst wenn eine Klasse versiegelt ist, die Referenz möglicherweise immer noch null ist und daher acallvirt
benötigt wird. Sie können dies umgehen (ohne die Klasse versiegeln zu müssen), aber es wird ein bisschen sinnlos.Strukturen werden verwendet,
call
weil sie nicht in Unterklassen unterteilt werden können und niemals null sind.Weitere Informationen finden Sie in dieser Frage:
Call und Callvirt
quelle
call
wird, sind: in der Situationnew T().Method()
fürstruct
Methoden, für nicht virtuelle Aufrufe vonvirtual
Methoden (wiebase.Virtual()
) oder fürstatic
Methoden. Überall sonst verwendetcallvirt
.Update: Ab .NET Core 2.0 und .NET Desktop 4.7.1 unterstützt die CLR jetzt die Devirtualisierung. Es kann Methoden in versiegelten Klassen verwenden und virtuelle Anrufe durch direkte Anrufe ersetzen - und dies kann es auch für nicht versiegelte Klassen tun, wenn es herausfindet, dass dies sicher ist.
In einem solchen Fall (eine versiegelte Klasse, die die CLR sonst nicht als sicher für die Devirtualisierung erkennen könnte) sollte eine versiegelte Klasse tatsächlich einen Leistungsvorteil bieten.
Trotzdem würde ich nicht denken, dass es sich lohnt, sich Sorgen zu machen, wenn Sie den Code nicht bereits profiliert und festgestellt hätten, dass Sie sich auf einem besonders heißen Weg befinden, der millionenfach aufgerufen wird, oder so ähnlich:
https://blogs.msdn.microsoft.com/dotnet/2017/06/29/performance-improvements-in-ryujit-in-net-core-and-net-framework/
Ursprüngliche Antwort:
Ich habe das folgende Testprogramm erstellt und es dann mit Reflector dekompiliert, um zu sehen, welcher MSIL-Code ausgegeben wurde.
In allen Fällen gibt der C # -Compiler (Visual Studio 2010 in der Release-Build-Konfiguration) identische MSIL aus, die wie folgt lautet:
Der oft zitierte Grund, warum die Leute sagen, dass Sealed Leistungsvorteile bietet, ist, dass der Compiler weiß, dass die Klasse nicht überschrieben wird, und sie daher verwenden kann,
call
anstattcallvirt
nach Virtuals usw. zu suchen. Wie oben gezeigt, ist dies nicht der Fall wahr.Mein nächster Gedanke war, dass der JIT-Compiler versiegelte Klassen möglicherweise anders behandelt, obwohl die MSIL identisch ist.
Ich habe einen Release-Build unter dem Visual Studio-Debugger ausgeführt und die dekompilierte x86-Ausgabe angezeigt. In beiden Fällen war der x86-Code identisch, mit Ausnahme von Klassennamen und Funktionsspeicheradressen (die natürlich unterschiedlich sein müssen). Hier ist es
Ich dachte dann, dass das Ausführen unter dem Debugger möglicherweise zu einer weniger aggressiven Optimierung führt.
Ich habe dann eine eigenständige Release-Build-ausführbare Datei außerhalb aller Debugging-Umgebungen ausgeführt und WinDBG + SOS verwendet, um nach Abschluss des Programms einzubrechen und die Auflösung des JIT-kompilierten x86-Codes anzuzeigen.
Wie Sie dem folgenden Code entnehmen können, ist der JIT-Compiler außerhalb des Debuggers aggressiver und hat die
WriteIt
Methode direkt in den Aufrufer integriert. Das Entscheidende ist jedoch, dass es identisch war, wenn eine versiegelte oder eine nicht versiegelte Klasse aufgerufen wurde. Es gibt keinerlei Unterschied zwischen einer versiegelten oder einer nicht versiegelten Klasse.Hier ist es beim Aufrufen einer normalen Klasse:
Vs eine versiegelte Klasse:
Für mich stellt dieser festen Beweis , dass es nicht kann jede Leistungsverbesserung zwischen Aufrufen von Methoden sein auf vs nicht-versiegelten Klassen versiegelt ... Ich glaube , ich bin jetzt glücklich :-)
quelle
callvirt
diese Methoden, wenn sie zunächst nicht virtuell sind?callvirt
für versiegelte Methoden verwendet wird, da das Objekt vor dem Aufrufen des Methodenaufrufs immer noch auf Null überprüft werden muss. Wenn Sie dies berücksichtigen, können Sie dies auch einfach tun verwendencallvirt
. Um das zu entfernencallvirt
und einfach direkt zu springen, müssten sie entweder C # ändern, um es((string)null).methodCall()
wie C ++ zuzulassen , oder sie müssten statisch beweisen, dass das Objekt nicht null ist (was sie tun konnten, aber nicht getan haben)Wie ich weiß, gibt es keine Garantie für Leistungsvorteile. Es besteht jedoch die Möglichkeit, die Leistungseinbußen unter bestimmten Bedingungen mit der versiegelten Methode zu verringern . (Versiegelte Klasse macht alle Methoden zu versiegeln.)
Es liegt jedoch an der Implementierungs- und Ausführungsumgebung des Compilers.
Einzelheiten
Viele moderne CPUs verwenden eine lange Pipeline-Struktur, um die Leistung zu steigern. Da die CPU unglaublich schneller als der Speicher ist, muss die CPU Code aus dem Speicher vorab abrufen, um die Pipeline zu beschleunigen. Wenn der Code nicht zum richtigen Zeitpunkt bereit ist, sind die Pipelines inaktiv.
Es gibt ein großes Hindernis namens Dynamic Dispatch, das diese "Prefetching" -Optimierung stört. Sie können dies nur als bedingte Verzweigung verstehen.
Die CPU kann in diesem Fall den nächsten auszuführenden Code nicht vorab abrufen, da die nächste Codeposition unbekannt ist, bis die Bedingung behoben ist. Dies führt zu Gefahren im Leerlauf der Pipeline. Und die Leistungseinbußen im Leerlauf sind regelmäßig enorm.
Ähnliches passiert beim Überschreiben von Methoden. Der Compiler bestimmt möglicherweise die richtige Methodenüberschreibung für den aktuellen Methodenaufruf, aber manchmal ist dies unmöglich. In diesem Fall kann die richtige Methode nur zur Laufzeit ermittelt werden. Dies ist auch ein Fall des dynamischen Versands, und ein Hauptgrund dafür, dass dynamisch typisierte Sprachen im Allgemeinen langsamer sind als statisch typisierte Sprachen.
Einige CPUs (einschließlich der neueren x86-Chips von Intel) verwenden eine als spekulative Ausführung bezeichnete Technik , um die Pipeline auch in bestimmten Situationen zu nutzen. Rufen Sie einfach einen der Ausführungspfade vor. Die Trefferquote dieser Technik ist jedoch nicht so hoch. Und Spekulationsfehler führen zum Stillstand der Pipeline, was ebenfalls einen enormen Leistungsverlust zur Folge hat. (Dies erfolgt vollständig durch CPU-Implementierung. Einige mobile CPUs sind bekannt, da diese Art der Optimierung nicht zur Energieeinsparung geeignet ist.)
Grundsätzlich ist C # eine statisch kompilierte Sprache. Aber nicht immer. Ich kenne den genauen Zustand nicht und dies hängt ganz von der Compiler-Implementierung ab. Einige Compiler können die Möglichkeit eines dynamischen Versands ausschließen, indem sie das Überschreiben von Methoden verhindern, wenn die Methode als markiert ist
sealed
. Dumme Compiler dürfen das nicht. Dies ist der Leistungsvorteil dessealed
.Diese Antwort ( Warum ist es schneller, ein sortiertes Array zu verarbeiten als ein unsortiertes Array? ) Beschreibt die Verzweigungsvorhersage viel besser.
quelle
<off-topic-rant>
Ich hasse versiegelte Klassen. Selbst wenn die Leistungsvorteile erstaunlich sind (was ich bezweifle), zerstören sie das objektorientierte Modell, indem sie die Wiederverwendung durch Vererbung verhindern. Beispielsweise ist die Thread-Klasse versiegelt. Ich kann zwar sehen, dass Threads so effizient wie möglich sein sollen, kann mir aber auch Szenarien vorstellen, in denen die Unterklasse von Threads große Vorteile hätte. Klassenautoren, wenn Sie Ihre Klassen aus "Leistungsgründen" versiegeln müssen , geben Sie bitte mindestens eine Schnittstelle an, damit wir nicht überall umbrechen und ersetzen müssen, wo wir eine Funktion benötigen, die Sie vergessen haben.
Beispiel: SafeThread musste die Thread-Klasse umbrechen , da Thread versiegelt ist und keine IThread-Schnittstelle vorhanden ist. SafeThread fängt automatisch nicht behandelte Ausnahmen in Threads ab, was in der Thread-Klasse vollständig fehlt. [und nein, die nicht behandelten Ausnahmeereignisse erfassen keine nicht behandelten Ausnahmen in sekundären Threads].
</ off-topic -rant>
quelle
Das Markieren einer Klasse
sealed
sollte keine Auswirkungen auf die Leistung haben.Es gibt Fälle, in denen
csc
möglicherweise eincallvirt
Opcode anstelle einescall
Opcodes ausgegeben werden muss. Es scheint jedoch, dass diese Fälle selten sind.Und es scheint mir, dass die JIT in der Lage sein sollte, denselben nicht-virtuellen Funktionsaufruf zu senden, für
callvirt
den siecall
es tun würde , wenn sie weiß, dass die Klasse (noch) keine Unterklassen hat. Wenn nur eine Implementierung der Methode vorhanden ist, macht es keinen Sinn, die Adresse aus einer vtable zu laden. Rufen Sie einfach die eine Implementierung direkt auf. In diesem Fall kann die JIT die Funktion sogar einbinden.Es ist ein bisschen wie ein Glücksspiel auf dem Teil des JIT, denn wenn eine Unterklasse wird später geladen, wird die JIT wegzuwerfen Code , dass die Maschine muß und den Code erneut kompiliert, eine echte virtuelle Call emittieren. Ich vermute, dass dies in der Praxis nicht oft vorkommt.
(Und ja, VM-Designer verfolgen diese winzigen Leistungsgewinne wirklich aggressiv.)
quelle
Versiegelte Klassen sollten eine Leistungsverbesserung bieten. Da eine versiegelte Klasse nicht abgeleitet werden kann, können virtuelle Mitglieder in nicht virtuelle Mitglieder umgewandelt werden.
Natürlich sprechen wir von wirklich kleinen Gewinnen. Ich würde eine Klasse nicht als versiegelt markieren, nur um eine Leistungsverbesserung zu erzielen, es sei denn, die Profilerstellung ergab, dass dies ein Problem darstellt.
quelle
call
anstelle voncallvirt
... Ich würde Nicht-Null-Referenztypen auch aus vielen anderen Gründen lieben ... Seufzer :-(Ich betrachte "versiegelte" Klassen als Normalfall und habe IMMER einen Grund, das Schlüsselwort "versiegelte" wegzulassen.
Die wichtigsten Gründe für mich sind:
a) Bessere Überprüfungen der Kompilierungszeit (Casting in nicht implementierte Schnittstellen wird zur Kompilierungszeit erkannt, nicht nur zur Laufzeit).
und, Hauptgrund:
b) Ein Missbrauch meiner Klassen ist auf diese Weise nicht möglich
Ich wünschte, Microsoft hätte "versiegelt" zum Standard gemacht, nicht "nicht versiegelt".
quelle
@ Vaibhav, welche Art von Tests haben Sie durchgeführt, um die Leistung zu messen?
Ich denke, man müsste Rotor verwenden und in CLI bohren und verstehen, wie eine versiegelte Klasse die Leistung verbessern würde.
quelle
Versiegelte Klassen sind mindestens ein kleines bisschen schneller, können aber manchmal sehr schnell sein ... wenn der JIT-Optimierer Anrufe einbinden kann, die sonst virtuelle Anrufe gewesen wären. Wenn es also häufig genannte Methoden gibt, die klein genug sind, um eingefügt zu werden, sollten Sie auf jeden Fall die Klasse versiegeln.
Der beste Grund, eine Klasse zu besiegeln, ist jedoch zu sagen: "Ich habe dies nicht so entworfen, dass es geerbt wird, also werde ich Sie nicht verbrennen lassen, wenn ich davon ausgehe, dass es so entworfen wurde, und ich werde es nicht tun." mich zu verbrennen, indem ich in eine Implementierung eingeschlossen werde, weil ich dich davon ableiten lasse. "
Ich weiß, dass einige hier gesagt haben, sie hassen versiegelte Klassen, weil sie die Möglichkeit haben wollen, von irgendetwas abzuleiten ... aber das ist OFT nicht die am besten zu wartende Wahl ... weil das Aussetzen einer Klasse der Ableitung Sie viel mehr einschließt, als nicht alle auszusetzen Das. Es ist ähnlich wie zu sagen: "Ich verabscheue Klassen mit privaten Mitgliedern ... Ich kann die Klasse oft nicht dazu bringen, das zu tun, was ich will, weil ich keinen Zugang habe." Einkapselung ist wichtig ... Versiegelung ist eine Form der Einkapselung.
quelle
callvirt
(virtueller Methodenaufruf) beispielsweise Methoden für versiegelte Klassen, da diese weiterhin einer Nullobjektprüfung unterzogen werden müssen. In Bezug auf Inlining kann (und tut) die CLR-JIT virtuelle Inline-Methodenaufrufe sowohl für versiegelte als auch für nicht versiegelte Klassen ... also ja. Die Performance-Sache ist ein Mythos.Um sie wirklich zu sehen, müssen Sie den JIT-kompilierten Code (letzter) analysieren .
C # -Code
MIL-Code
JIT-kompilierter Code
Während die Erstellung der Objekte identisch ist, unterscheiden sich die Anweisungen zum Aufrufen der Methoden der versiegelten und abgeleiteten / Basisklasse geringfügig. Führen Sie nach dem Verschieben von Daten in Register oder RAM (mov-Befehl) beim Aufrufen der versiegelten Methode einen Vergleich zwischen dword ptr [ecx], ecx (cmp-Befehl) aus und rufen Sie die Methode auf, während die abgeleitete / Basisklasse die Methode direkt ausführt. .
Laut dem Bericht von Torbjörn Granlund, Befehlslatenzen und Durchsatz für AMD- und Intel x86-Prozessoren , beträgt die Geschwindigkeit des folgenden Befehls in einem Intel Pentium 4:
Link : https://gmplib.org/~tege/x86-timing.pdf
Dies bedeutet, dass die zum Aufrufen einer versiegelten Methode erforderliche Zeit im Idealfall 2 Zyklen beträgt, während die zum Aufrufen einer abgeleiteten Methode oder einer Basisklassenmethode erforderliche Zeit 3 Zyklen beträgt.
Die Optimierung der Compiler hat den Unterschied zwischen den Leistungen einer versiegelten und einer nicht versiegelten Klasse so gering gemacht, dass es sich um Prozessorkreise handelt, die aus diesem Grund für die meisten Anwendungen irrelevant sind.
quelle
Wenn Sie diesen Code ausführen, werden Sie feststellen, dass versiegelte Klassen zweimal schneller sind:
Ausgabe: Versiegelte Klasse: 00: 00: 00.1897568 Nicht versiegelte Klasse: 00: 00: 00.3826678
quelle