.net - Bieten versiegelte Klassen wirklich Leistungsvorteile?

Translate

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

This question and all comments follow the "Attribution Required."

Alle Antworten

Translate

Der JITter verwendet manchmal nicht virtuelle Aufrufe von Methoden in versiegelten Klassen, da sie nicht weiter erweitert werden können.

Es gibt komplexe Regeln bezüglich des Aufruftyps "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:Streifzug durch das versiegelte Schlüsselwort

Quelle
Translate

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

Das Problem kommt auf diecallvs.callvirtIL-Operationscodes.Callist schneller alscallvirt, undcallvirtwird 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 ändern, wenn Sie eine Klasse versiegelncalvirtszucallsund wird schneller sein.

Unglücklicherweisecallvirtmacht andere Dinge, die es auch 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 somit acallvirtwird gebraucht. Sie können dies umgehen (ohne die Klasse versiegeln zu müssen), aber es wird ein bisschen sinnlos.

Strukturen verwendencallweil sie nicht untergeordnet werden können und niemals null sind.

Weitere Informationen finden Sie in dieser Frage:

Call und Callvirt

Quelle
Translate

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 dies 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 machenes sei dennSie hatten den Code bereits profiliert und festgestellt, dass Sie sich auf einem besonders heißen Pfad 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, von dem die Leute sagen, dass Sealed Leistungsvorteile bietet, ist, dass der Compiler weiß, dass die Klasse nicht überschrieben wird und daher verwendet werden kanncallAnstatt voncallvirtDa es nicht nach Virtuals usw. suchen muss, ist dies nicht der Fall.

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 den Code eingebundenWriteItMethode direkt in den Anrufer. 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 ist dies ein solider Beweis dafürkann nichtSeien Sie eine Leistungsverbesserung zwischen den Aufrufmethoden für versiegelte und nicht versiegelte Klassen ... Ich denke, ich bin jetzt glücklich :-)

Quelle
Translate

Wie ich weiß, gibt es keine Garantie für Leistungsvorteile. Aber da isteine Chance, die Leistungseinbußen unter bestimmten Bedingungen zu verringernmit versiegelter Methode. (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 namensdynamischer VersandDies stört diese "Prefetching" -Optimierung. 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. Das macht alsoGefahrverursacht Pipeline-Leerlauf. 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 neuesten Intel x86-Chips) verwenden die sogenannte Technikspekulative AusführungPipeline auch auf die Situation 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 istsealed. 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
Translate

<off-topic-rant>

I verabscheuenversiegelte Klassen. Auch wenn die Leistungsvorteile erstaunlich sind (was ich bezweifle), sind sie eszerstörendas objektorientierte Modell durch Verhinderung der Wiederverwendung durch Vererbung. Beispielsweise ist die Thread-Klasse versiegelt. Ich kann zwar sehen, dass Threads so effizient wie möglich sein sollen, aber ich kann mir auch Szenarien vorstellen, in denen die Unterklasse von Threads große Vorteile hätte. Klassenautoren, wenn SieMussVersiegeln Sie Ihre Klassen aus "Leistungsgründen",Bitte geben Sie eine Schnittstelle anZumindest müssen wir also nicht überall einpacken und ersetzen, wo wir eine Funktion benötigen, die Sie vergessen haben.

Beispiel:SafeThreadmusste 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 tun diesnichtnicht behandelte Ausnahmen in sekundären Threads aufnehmen].

</ off-topic -rant>

Quelle
Translate

Eine Klasse markierensealedsollte keine Auswirkungen auf die Leistung haben.

Es gibt Fälle, in denencscMöglicherweise muss acallvirtOpcode statt acallOpcode. 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 für zu sendencallvirtdas wäre es fürcall, wenn es 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 ein Glücksspiel seitens der JIT, denn wenn es sich um eine Unterklasse handeltisSpäter geladen, muss die JIT diesen Maschinencode wegwerfen und den Code erneut kompilieren, wodurch ein realer virtueller Aufruf ausgegeben wird. Ich vermute, dass dies in der Praxis nicht oft vorkommt.

(Und ja, VM-Designer verfolgen diese winzigen Leistungsgewinne wirklich aggressiv.)

Quelle
Translate

Versiegelte Klassensolltebieten eine Leistungsverbesserung. 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
Translate

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
Translate

@ Vaibhav, welche Art von Tests haben Sie durchgeführt, um die Leistung zu messen?

Ich denke man müsste es benutzenRotorund in CLI zu bohren und zu verstehen, wie eine versiegelte Klasse die Leistung verbessern würde.

SSCLI (Rotor)
SSCLI: Shared Source Common Language Infrastructure

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.

Quelle
Translate

Versiegelte Klassen sind mindestens ein kleines bisschen schneller, können aber manchmal sehr schnell sein ... wenn das JIT-Optimierungsprogramm Anrufe einbinden kann, die ansonsten 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 hasse 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
Translate

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

Quelle