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, call
anstatt callvirt
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
// 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 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:
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 :-)