Bieten versiegelte Klassen wirklich Leistungsvorteile?


140

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 :)


8
Ich glaube nicht, dass versiegelte Klassen dazu gedacht waren, die Leistung zu steigern. Die Tatsache, dass sie es tun, kann zufällig sein. Profilieren Sie außerdem Ihre App, nachdem Sie sie für die Verwendung versiegelter Klassen überarbeitet haben, und stellen Sie fest, ob sich die Mühe gelohnt hat. Das Sperren Ihrer Erweiterbarkeit für eine nicht benötigte Mikrooptimierung kostet Sie auf lange Sicht. Wenn Sie ein Profil erstellt haben und damit Ihre Perf-Benchmarks erreichen können (und nicht um der Perfektion willen), können Sie als Team entscheiden, ob es das ausgegebene Geld wert ist. Wenn Sie Klassen aus nicht perfekten Gründen versiegelt haben, dann behalten Sie sie :)
Merlyn Morgan-Graham

1
Hast du es mit Nachdenken versucht? Ich habe irgendwo gelesen, dass das Instanziieren durch Reflexion mit versiegelten Klassen schneller ist
Uhr

Meines Wissens gibt es keine. Sealed gibt es aus einem anderen Grund - um die Erweiterbarkeit zu blockieren, was in vielen Fällen nützlich / erforderlich sein kann. Leistungsoptimierung war hier kein Ziel.
TomTom

... aber wenn Sie an den Compiler denken: Wenn Ihre Klasse versiegelt ist, kennen Sie die Adresse einer Methode, die Sie zur Kompilierungszeit für Ihre Klasse aufrufen. Wenn Ihre Klasse nicht versiegelt ist, müssen Sie die Methode zur Laufzeit auflösen, da Sie möglicherweise eine Überschreibung aufrufen müssen. Es wird sicherlich vernachlässigbar sein, aber ich konnte sehen, dass es einen Unterschied geben würde .
Dan Puzey

Ja, aber diese Art von führt nicht zu echten Vorteilen, wie das OP hervorgehoben hat. Architektonische Unterschiede sind / können viel relevanter sein.
TomTom

Antworten:


59

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


2
Der 'Rambling'-Link ist insofern interessant, als er nach technischer Güte klingt, aber tatsächlich Unsinn ist. Lesen Sie die Kommentare zum Artikel für weitere Informationen. Zusammenfassung: Die 3 angegebenen Gründe sind Versionierung, Leistung und Sicherheit / Vorhersagbarkeit - [siehe nächster Kommentar]
Steven A. Lowe

1
[Fortsetzung] Die Versionierung gilt nur, wenn es keine Unterklassen gibt, duh, aber erweitern Sie dieses Argument auf jede Klasse und plötzlich haben Sie keine Vererbung mehr und raten Sie mal, die Sprache ist nicht mehr objektorientiert (sondern nur objektbasiert)! [siehe nächste]
Steven A. Lowe

3
[Fortsetzung] Das Leistungsbeispiel ist ein Witz: Optimierung eines virtuellen Methodenaufrufs; Warum sollte eine versiegelte Klasse überhaupt eine virtuelle Methode haben, da sie nicht in Unterklassen unterteilt werden kann? Schließlich ist das Argument Sicherheit / Vorhersagbarkeit eindeutig falsch: "Sie können es nicht verwenden, damit es sicher / vorhersehbar ist." LOL!
Steven A. Lowe

16
@Steven A. Lowe - Ich denke, Jeffrey Richter hat versucht, ein wenig umständlich zu sagen, dass Sie, wenn Sie Ihre Klasse nicht versiegelt lassen, darüber nachdenken müssen, wie abgeleitete Klassen sie verwenden können / werden und ob Sie die nicht haben Zeit oder Neigung, dies richtig zu tun, und versiegeln Sie es dann, da es in Zukunft weniger wahrscheinlich ist, dass Änderungen im Code anderer Personen auftreten. Das ist überhaupt kein Unsinn, es ist gesunder Menschenverstand.
Greg Beech

6
Eine versiegelte Klasse verfügt möglicherweise über eine virtuelle Methode, da sie möglicherweise von einer Klasse abgeleitet ist, die sie deklariert. Wenn Sie dann später eine Variable der versiegelten Nachkommenklasse deklarieren und diese Methode aufrufen, gibt der Compiler möglicherweise einen direkten Aufruf an die bekannte Implementierung aus, da er weiß, dass dies auf keinen Fall anders sein kann als in der bekannten Implementierung. vtable zur Kompilierungszeit für diese Klasse. Versiegelt / nicht versiegelt ist dies eine andere Diskussion, und ich stimme den Gründen zu, warum Klassen standardmäßig versiegelt werden.
Lasse V. Karlsen

143

Die Antwort lautet: Nein, versiegelte Klassen schneiden nicht besser ab als nicht versiegelte.

Das Problem liegt in den callvs callvirtIL-Operationscodes. Callist schneller als callvirtund callvirtwird 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 von calvirtszu ändern callsund schneller sind , wenn Sie eine Klasse versiegeln .

Leider callvirtmacht 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 a callvirtbenötigt wird. Sie können dies umgehen (ohne die Klasse versiegeln zu müssen), aber es wird ein bisschen sinnlos.

Strukturen werden verwendet, callweil sie nicht in Unterklassen unterteilt werden können und niemals null sind.

Weitere Informationen finden Sie in dieser Frage:

Call und Callvirt


5
AFAIK, die Umstände, unter denen verwendet callwird, sind: in der Situation new T().Method()für structMethoden, für nicht virtuelle Aufrufe von virtualMethoden (wie base.Virtual()) oder für staticMethoden. Überall sonst verwendet callvirt.
porges

1
Ähh ... Mir ist klar, dass dies alt ist, aber das ist nicht ganz richtig ... der große Gewinn bei versiegelten Klassen ist, wenn der JIT-Optimierer den Anruf einbinden kann ... in diesem Fall kann die versiegelte Klasse ein großer Gewinn sein .
Brian Kennedy

5
Warum diese Antwort falsch ist. Aus Mono Changelog: "Devirtualisierungsoptimierung für versiegelte Klassen und Methoden, Verbesserung der IronPython 2.0-Pystone-Leistung um 4%. Andere Programme können eine ähnliche Verbesserung erwarten [Rodrigo]." Versiegelte Klassen können die Leistung verbessern, aber wie immer hängt es von der Situation ab.
Smilediver

1
@ Smilediver Es kann die Leistung verbessern, aber nur, wenn Sie eine schlechte JIT haben (keine Ahnung, wie gut die .NET-JITs heutzutage sind - früher war diesbezüglich ziemlich schlecht). Hotspot beispielsweise wird virtuelle Anrufe inline einbinden und später bei Bedarf deoptimieren. Daher zahlen Sie den zusätzlichen Aufwand nur, wenn Sie die Klasse tatsächlich unterordnen (und selbst dann nicht unbedingt).
Voo

1
-1 Die JIT muss nicht unbedingt denselben Maschinencode für dieselben IL-Opcodes generieren. Nullprüfung und virtueller Aufruf sind orthogonale und separate Schritte von callvirt. Bei versiegelten Typen kann der JIT-Compiler einen Teil von callvirt weiterhin optimieren. Gleiches gilt, wenn der JIT-Compiler garantieren kann, dass eine Referenz nicht null ist.
Rechtsfalte

29

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.

public class NormalClass {
    public void WriteIt(string x) {
        Console.WriteLine("NormalClass");
        Console.WriteLine(x);
    }
}

public sealed class SealedClass {
    public void WriteIt(string x) {
        Console.WriteLine("SealedClass");
        Console.WriteLine(x);
    }
}

public static void CallNormal() {
    var n = new NormalClass();
    n.WriteIt("a string");
}

public static void CallSealed() {
    var n = new SealedClass();
    n.WriteIt("a string");
}

In allen Fällen gibt der C # -Compiler (Visual Studio 2010 in der Release-Build-Konfiguration) identische MSIL aus, die wie folgt lautet:

L_0000: newobj instance void <NormalClass or SealedClass>::.ctor()
L_0005: stloc.0 
L_0006: ldloc.0 
L_0007: ldstr "a string"
L_000c: callvirt instance void <NormalClass or SealedClass>::WriteIt(string)
L_0011: ret 

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, callanstatt callvirtnach 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

//            var n = new NormalClass();
00000000  push        ebp 
00000001  mov         ebp,esp 
00000003  sub         esp,8 
00000006  cmp         dword ptr ds:[00585314h],0 
0000000d  je          00000014 
0000000f  call        70032C33 
00000014  xor         edx,edx 
00000016  mov         dword ptr [ebp-4],edx 
00000019  mov         ecx,588230h 
0000001e  call        FFEEEBC0 
00000023  mov         dword ptr [ebp-8],eax 
00000026  mov         ecx,dword ptr [ebp-8] 
00000029  call        dword ptr ds:[00588260h] 
0000002f  mov         eax,dword ptr [ebp-8] 
00000032  mov         dword ptr [ebp-4],eax 
//            n.WriteIt("a string");
00000035  mov         edx,dword ptr ds:[033220DCh] 
0000003b  mov         ecx,dword ptr [ebp-4] 
0000003e  cmp         dword ptr [ecx],ecx 
00000040  call        dword ptr ds:[0058827Ch] 
//        }
00000046  nop 
00000047  mov         esp,ebp 
00000049  pop         ebp 
0000004a  ret 

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 WriteItMethode 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:

Normal JIT generated code
Begin 003c00b0, size 39
003c00b0 55              push    ebp
003c00b1 8bec            mov     ebp,esp
003c00b3 b994391800      mov     ecx,183994h (MT: ScratchConsoleApplicationFX4.NormalClass)
003c00b8 e8631fdbff      call    00172020 (JitHelp: CORINFO_HELP_NEWSFAST)
003c00bd e80e70106f      call    mscorlib_ni+0x2570d0 (6f4c70d0) (System.Console.get_Out(), mdToken: 060008fd)
003c00c2 8bc8            mov     ecx,eax
003c00c4 8b1530203003    mov     edx,dword ptr ds:[3302030h] ("NormalClass")
003c00ca 8b01            mov     eax,dword ptr [ecx]
003c00cc 8b403c          mov     eax,dword ptr [eax+3Ch]
003c00cf ff5010          call    dword ptr [eax+10h]
003c00d2 e8f96f106f      call    mscorlib_ni+0x2570d0 (6f4c70d0) (System.Console.get_Out(), mdToken: 060008fd)
003c00d7 8bc8            mov     ecx,eax
003c00d9 8b1534203003    mov     edx,dword ptr ds:[3302034h] ("a string")
003c00df 8b01            mov     eax,dword ptr [ecx]
003c00e1 8b403c          mov     eax,dword ptr [eax+3Ch]
003c00e4 ff5010          call    dword ptr [eax+10h]
003c00e7 5d              pop     ebp
003c00e8 c3              ret

Vs eine versiegelte Klasse:

Normal JIT generated code
Begin 003c0100, size 39
003c0100 55              push    ebp
003c0101 8bec            mov     ebp,esp
003c0103 b90c3a1800      mov     ecx,183A0Ch (MT: ScratchConsoleApplicationFX4.SealedClass)
003c0108 e8131fdbff      call    00172020 (JitHelp: CORINFO_HELP_NEWSFAST)
003c010d e8be6f106f      call    mscorlib_ni+0x2570d0 (6f4c70d0) (System.Console.get_Out(), mdToken: 060008fd)
003c0112 8bc8            mov     ecx,eax
003c0114 8b1538203003    mov     edx,dword ptr ds:[3302038h] ("SealedClass")
003c011a 8b01            mov     eax,dword ptr [ecx]
003c011c 8b403c          mov     eax,dword ptr [eax+3Ch]
003c011f ff5010          call    dword ptr [eax+10h]
003c0122 e8a96f106f      call    mscorlib_ni+0x2570d0 (6f4c70d0) (System.Console.get_Out(), mdToken: 060008fd)
003c0127 8bc8            mov     ecx,eax
003c0129 8b1534203003    mov     edx,dword ptr ds:[3302034h] ("a string")
003c012f 8b01            mov     eax,dword ptr [ecx]
003c0131 8b403c          mov     eax,dword ptr [eax+3Ch]
003c0134 ff5010          call    dword ptr [eax+10h]
003c0137 5d              pop     ebp
003c0138 c3              ret

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 :-)


Gibt es einen Grund, warum Sie diese Antwort zweimal gepostet haben, anstatt die andere als Duplikat zu schließen? Sie sehen für mich wie gültige Duplikate aus, obwohl dies nicht mein Bereich ist.
Flexo

1
Was wäre, wenn die Methoden länger wären (mehr als 32 Byte IL-Code), um Inlining zu vermeiden und zu sehen, welche Aufrufoperation verwendet werden würde? Wenn es inline ist, sehen Sie den Anruf nicht und können die Auswirkungen nicht beurteilen.
Ygoe

Ich bin verwirrt: Warum gibt es callvirtdiese Methoden, wenn sie zunächst nicht virtuell sind?
freakish

@freakish Ich kann mich nicht erinnern, wo ich das gesehen habe, aber ich habe gelesen, dass die CLR callvirtfü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 verwenden callvirt. Um das zu entfernen callvirtund 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)
Orion Edwards

1
Wir empfehlen Ihnen, sich auf die Ebene des Maschinencodes zu konzentrieren, aber seien Sie sehr vorsichtig , wenn Sie Aussagen wie "Dies ist ein solider Beweis dafür, dass es keine Leistungsverbesserung geben kann " machen. Was Sie gezeigt haben, ist, dass es für ein bestimmtes Szenario keinen Unterschied in der nativen Ausgabe gibt. Es ist ein Datenpunkt, und man kann nicht davon ausgehen, dass er sich auf alle Szenarien verallgemeinert. Für den Anfang definieren Ihre Klassen keine virtuellen Methoden, sodass virtuelle Aufrufe überhaupt nicht erforderlich sind.
Matt Craig

24

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.

// Value of `v` is unknown,
// and can be resolved only at runtime.
// CPU cannot know which code to prefetch.
// Therefore, just prefetch any one of a() or b().
// This is *speculative execution*.
int v = random();
if (v==1) a();
else b();

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 des sealed.


Diese Antwort ( Warum ist es schneller, ein sortiertes Array zu verarbeiten als ein unsortiertes Array? ) Beschreibt die Verzweigungsvorhersage viel besser.


1
CPUs der Pentium-Klasse rufen den indirekten Versand direkt vor. Manchmal ist die Umleitung von Funktionszeigern aus diesem Grund schneller als wenn (nicht erratbar).
Joshua

2
Ein Vorteil nicht virtueller oder versiegelter Funktionen besteht darin, dass sie in mehreren Situationen eingebunden werden können.
CodesInChaos

4

<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>


35
Ich versiegele meine Klassen aus Leistungsgründen nicht. Ich versiegele sie aus Designgründen. Das Entwerfen für die Vererbung ist schwierig, und dieser Aufwand wird die meiste Zeit verschwendet. Ich stimme jedoch der Bereitstellung von Schnittstellen voll und ganz zu - das ist eine weitaus bessere Lösung für das Entsiegeln von Klassen.
Jon Skeet

6
Die Kapselung ist im Allgemeinen eine bessere Lösung als die Vererbung. Um Ihr spezifisches Thread-Beispiel zu nennen: Das Überfüllen von Thread-Ausnahmen verstößt gegen das Liskov-Substitutionsprinzip, da Sie das dokumentierte Verhalten der Thread-Klasse geändert haben. Selbst wenn Sie daraus ableiten könnten, wäre es nicht sinnvoll zu sagen, dass Sie SafeThread überall verwenden könnten Sie könnten Thread verwenden. In diesem Fall ist es besser, Thread in eine andere Klasse zu kapseln, die ein anderes dokumentiertes Verhalten aufweist, was Sie tun können. Manchmal sind die Dinge zu deinem eigenen Besten versiegelt.
Greg Beech

1
@ [Greg Beech]: Meinung, keine Tatsache - von Thread erben zu können, um ein abscheuliches Versehen in seinem Design zu beheben, ist KEINE schlechte Sache ;-) Und ich denke, Sie übertreiben LSP - die nachweisbare Eigenschaft q (x) in Dieser Fall ist 'eine nicht behandelte Ausnahme zerstört das Programm', was keine "wünschenswerte Eigenschaft" ist :-)
Steven A. Lowe

1
Nein, aber ich hatte meinen Anteil an beschissenem Code, bei dem ich anderen Entwicklern ermöglichte, meine Inhalte zu missbrauchen, indem ich sie nicht versiegelte oder Eckfälle zuließ. Der größte Teil meines Codes besteht heutzutage aus Behauptungen und anderen vertragsbezogenen Dingen. Und ich bin ziemlich offen für die Tatsache, dass ich das nur tue, um ein Schmerz im Arsch zu sein.
Turing Complete

2
Da wir tun Wegthema Erfreuliches hier, so wie du verabscheuen versiegelte Klassen, ich verabscheue geschluckt Ausnahmen. Es gibt nichts Schlimmeres, als wenn etwas schief geht, aber das Programm geht weiter. JavaScript ist mein Favorit. Sie nehmen eine Änderung an einem Code vor und plötzlich macht das Klicken auf eine Schaltfläche absolut nichts. Toll! ASP.NET und UpdatePanel sind andere; Ernsthaft, wenn mein Button-Handler wirft, ist es eine große Sache und es muss abstürzen, damit ich weiß, dass es etwas gibt, das repariert werden muss! Eine Schaltfläche , die nichts tut , ist mehr nutzlos als eine Schaltfläche , die einen Absturz Bildschirm bringt!
Roman Starkov

4

Das Markieren einer Klasse sealedsollte keine Auswirkungen auf die Leistung haben.

Es gibt Fälle, in denen cscmöglicherweise ein callvirtOpcode anstelle eines callOpcodes 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 callvirtden sie calles 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.)


3

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.


Sie sollten , aber es scheint, dass sie es nicht tun. Wenn die CLR das Konzept von Nicht-Null-Referenztypen hätte, wäre eine versiegelte Klasse wirklich besser, da der Compiler callanstelle von callvirt... Ich würde Nicht-Null-Referenztypen auch aus vielen anderen Gründen lieben ... Seufzer :-(
Orion Edwards

3

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".


Ich glaube, "weglassen" (weglassen) sollte "emittieren" (produzieren) sein?
Benutzer2864740

2

@ 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.

SSCLI (Rotor)
SSCLI: Gemeinsame Sprachinfrastruktur für gemeinsam genutzte Quellen

Die Common Language Infrastructure (CLI) ist der ECMA-Standard, der den Kern von .NET Framework beschreibt. Die Shared Source CLI (SSCLI), auch als Rotor bekannt, ist ein komprimiertes Archiv des Quellcodes für eine funktionierende Implementierung der ECMA CLI und der ECMA C # -Sprachenspezifikation, Technologien, die das Herzstück der .NET-Architektur von Microsoft bilden.


Der Test beinhaltete das Erstellen einer Klassenhierarchie mit einigen Methoden, die Dummy-Arbeit erledigten (meistens String-Manipulation). Einige dieser Methoden waren virtuell. Sie riefen sich hier und da an. Rufen Sie diese Methoden dann 100, 10000 und 100000 Mal auf ... und messen Sie die verstrichene Zeit. Führen Sie diese dann aus, nachdem Sie die Klassen als versiegelt markiert haben. und wieder messen. Kein Unterschied in ihnen.
Vaibhav

2

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.


Der C # -Compiler verwendet weiterhin 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.
Orion Edwards

1

Um sie wirklich zu sehen, müssen Sie den JIT-kompilierten Code (letzter) analysieren .

C # -Code

public sealed class Sealed
{
    public string Message { get; set; }
    public void DoStuff() { }
}
public class Derived : Base
{
    public sealed override void DoStuff() { }
}
public class Base
{
    public string Message { get; set; }
    public virtual void DoStuff() { }
}
static void Main()
{
    Sealed sealedClass = new Sealed();
    sealedClass.DoStuff();
    Derived derivedClass = new Derived();
    derivedClass.DoStuff();
    Base BaseClass = new Base();
    BaseClass.DoStuff();
}

MIL-Code

.method private hidebysig static void  Main() cil managed
{
  .entrypoint
  // Code size       41 (0x29)
  .maxstack  8
  IL_0000:  newobj     instance void ConsoleApp1.Program/Sealed::.ctor()
  IL_0005:  callvirt   instance void ConsoleApp1.Program/Sealed::DoStuff()
  IL_000a:  newobj     instance void ConsoleApp1.Program/Derived::.ctor()
  IL_000f:  callvirt   instance void ConsoleApp1.Program/Base::DoStuff()
  IL_0014:  newobj     instance void ConsoleApp1.Program/Base::.ctor()
  IL_0019:  callvirt   instance void ConsoleApp1.Program/Base::DoStuff()
  IL_0028:  ret
} // end of method Program::Main

JIT-kompilierter Code

--- C:\Users\Ivan Porta\source\repos\ConsoleApp1\Program.cs --------------------
        {
0066084A  in          al,dx  
0066084B  push        edi  
0066084C  push        esi  
0066084D  push        ebx  
0066084E  sub         esp,4Ch  
00660851  lea         edi,[ebp-58h]  
00660854  mov         ecx,13h  
00660859  xor         eax,eax  
0066085B  rep stos    dword ptr es:[edi]  
0066085D  cmp         dword ptr ds:[5842F0h],0  
00660864  je          0066086B  
00660866  call        744CFAD0  
0066086B  xor         edx,edx  
0066086D  mov         dword ptr [ebp-3Ch],edx  
00660870  xor         edx,edx  
00660872  mov         dword ptr [ebp-48h],edx  
00660875  xor         edx,edx  
00660877  mov         dword ptr [ebp-44h],edx  
0066087A  xor         edx,edx  
0066087C  mov         dword ptr [ebp-40h],edx  
0066087F  nop  
            Sealed sealedClass = new Sealed();
00660880  mov         ecx,584E1Ch  
00660885  call        005730F4  
0066088A  mov         dword ptr [ebp-4Ch],eax  
0066088D  mov         ecx,dword ptr [ebp-4Ch]  
00660890  call        00660468  
00660895  mov         eax,dword ptr [ebp-4Ch]  
00660898  mov         dword ptr [ebp-3Ch],eax  
            sealedClass.DoStuff();
0066089B  mov         ecx,dword ptr [ebp-3Ch]  
0066089E  cmp         dword ptr [ecx],ecx  
006608A0  call        00660460  
006608A5  nop  
            Derived derivedClass = new Derived();
006608A6  mov         ecx,584F3Ch  
006608AB  call        005730F4  
006608B0  mov         dword ptr [ebp-50h],eax  
006608B3  mov         ecx,dword ptr [ebp-50h]  
006608B6  call        006604A8  
006608BB  mov         eax,dword ptr [ebp-50h]  
006608BE  mov         dword ptr [ebp-40h],eax  
            derivedClass.DoStuff();
006608C1  mov         ecx,dword ptr [ebp-40h]  
006608C4  mov         eax,dword ptr [ecx]  
006608C6  mov         eax,dword ptr [eax+28h]  
006608C9  call        dword ptr [eax+10h]  
006608CC  nop  
            Base BaseClass = new Base();
006608CD  mov         ecx,584EC0h  
006608D2  call        005730F4  
006608D7  mov         dword ptr [ebp-54h],eax  
006608DA  mov         ecx,dword ptr [ebp-54h]  
006608DD  call        00660490  
006608E2  mov         eax,dword ptr [ebp-54h]  
006608E5  mov         dword ptr [ebp-44h],eax  
            BaseClass.DoStuff();
006608E8  mov         ecx,dword ptr [ebp-44h]  
006608EB  mov         eax,dword ptr [ecx]  
006608ED  mov         eax,dword ptr [eax+28h]  
006608F0  call        dword ptr [eax+10h]  
006608F3  nop  
        }
0066091A  nop  
0066091B  lea         esp,[ebp-0Ch]  
0066091E  pop         ebx  
0066091F  pop         esi  
00660920  pop         edi  
00660921  pop         ebp  

00660922  ret  

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:

  • mov : hat 1 Zyklus als Latenz und der Prozessor kann 2,5 Befehle pro Zyklus dieses Typs aufrechterhalten
  • cmp : hat 1 Zyklus als Latenz und der Prozessor kann 2 Befehle pro Zyklus dieses Typs aufrechterhalten

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.


-10

Wenn Sie diesen Code ausführen, werden Sie feststellen, dass versiegelte Klassen zweimal schneller sind:

class Program
{
    static void Main(string[] args)
    {
        Console.ReadLine();

        var watch = new Stopwatch();
        watch.Start();
        for (int i = 0; i < 10000000; i++)
        {
            new SealedClass().GetName();
        }
        watch.Stop();
        Console.WriteLine("Sealed class : {0}", watch.Elapsed.ToString());

        watch.Start();
        for (int i = 0; i < 10000000; i++)
        {
            new NonSealedClass().GetName();
        }
        watch.Stop();
        Console.WriteLine("NonSealed class : {0}", watch.Elapsed.ToString());

        Console.ReadKey();
    }
}

sealed class SealedClass
{
    public string GetName()
    {
        return "SealedClass";
    }
}

class NonSealedClass
{
    public string GetName()
    {
        return "NonSealedClass";
    }
}

Ausgabe: Versiegelte Klasse: 00: 00: 00.1897568 Nicht versiegelte Klasse: 00: 00: 00.3826678


9
Es gibt ein paar Probleme damit. Zunächst einmal setzen Sie die Stoppuhr zwischen dem ersten und dem zweiten Test nicht zurück. Zweitens bedeutet die Art und Weise, wie Sie die Methode aufrufen, dass alle Op-Codes nicht callvirt aufgerufen werden, sodass der Typ keine Rolle spielt.
Cameron MacFarland

1
RuslanG, Sie haben vergessen, watch.Reset () aufzurufen, nachdem Sie den ersten Test ausgeführt haben. o_O:]

Ja, der obige Code weist Fehler auf. Andererseits, wer kann damit prahlen, niemals Fehler in den Code einzuführen? Aber diese Antwort hat eine wichtige Sache besser als alle anderen: Sie versucht, den fraglichen Effekt zu messen. Und diese Antwort teilt auch den Code für alle, die Sie inspizieren und [Massen-] Abstimmungen durchführen (ich frage mich, warum die Massenabstimmungen notwendig sind?). Im Gegensatz zu anderen Antworten. Das verdient meiner Meinung nach Respekt ... Bitte beachten Sie auch, dass der Benutzer ein Neuling ist, also auch kein sehr einladender Ansatz. Einige einfache Korrekturen und dieser Code wird für uns alle nützlich. Fix + Share feste Version, wenn Sie sich trauen
Roland Pihlakas

Ich bin nicht einverstanden mit @RolandPihlakas. Wie Sie sagten, "wer kann damit prahlen, niemals Fehler in den Code einzuführen". Antwort -> Nein, keine. Aber darum geht es nicht. Der Punkt ist, es ist eine falsche Information, die einen anderen neuen Programmierer irreführen kann. Viele Menschen können leicht übersehen, dass die Stoppuhr nicht zurückgesetzt wurde. Sie könnten glauben, dass die Benchmark-Informationen wahr sind. Ist das nicht schädlicher? Jemand, der schnell nach einer Antwort sucht, liest möglicherweise nicht einmal diese Kommentare, sondern er / sie würde eine Antwort sehen und er / sie könnte das glauben.
Emran Hussain
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.