Mise à jour: à partir de .NET Core 2.0 et .NET Desktop 4.7.1, le CLR prend désormais en charge la dévirtualisation. Il peut prendre des méthodes dans des classes scellées et remplacer les appels virtuels par des appels directs - et il peut également le faire pour les classes non scellées s'il peut comprendre qu'il est sûr de le faire.
Dans un tel cas (une classe scellée que le CLR ne pourrait autrement pas détecter comme sûre à dévirtualiser), une classe scellée devrait en fait offrir une sorte d'avantage de performance.
Cela dit, je ne pense pas qu'il vaudrait la peine de s'inquiéter à moins que vous n'ayez déjà profilé le code et déterminé que vous étiez dans un chemin particulièrement chaud en étant appelé des millions de fois, ou quelque chose comme ça:
https://blogs.msdn.microsoft.com/dotnet/2017/06/29/performance-improvements-in-ryujit-in-net-core-and-net-framework/
Réponse originale:
J'ai créé le programme de test suivant, puis je l'ai décompilé à l'aide de Reflector pour voir quel code MSIL a été émis.
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");
}
Dans tous les cas, le compilateur C # (Visual studio 2010 dans la configuration de build Release) émet le même MSIL, qui se présente comme suit:
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
La raison souvent citée pour laquelle les gens disent que scellé fournit des avantages en termes de performances est que le compilateur sait que la classe n'est pas surchargée et peut donc utiliser à la call
place decallvirt
termes de car il n'a pas à vérifier les virtuels, etc. Comme démontré ci-dessus, ce n'est pas vrai.
Ma pensée suivante était que même si le MSIL est identique, peut-être que le compilateur JIT traite les classes scellées différemment?
J'ai exécuté une version de version sous le débogueur de Visual Studio et j'ai vu la sortie x86 décompilée. Dans les deux cas, le code x86 était identique, à l'exception des noms de classe et des adresses de mémoire de fonction (qui bien sûr doivent être différents). C'est ici
// 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
J'ai alors pensé que peut-être courir sous le débogueur le faisait effectuer une optimisation moins agressive?
J'ai ensuite exécuté un exécutable de construction de version autonome en dehors de tout environnement de débogage, et j'ai utilisé WinDBG + SOS pour entrer par effraction une fois le programme terminé et afficher le désassemblage du code x86 compilé par JIT.
Comme vous pouvez le voir dans le code ci-dessous, lors de l'exécution en dehors du débogueur, le compilateur JIT est plus agressif et il a incorporé la WriteIt
méthode directement dans l'appelant. La chose cruciale est cependant qu'elle était identique lors de l'appel d'une classe scellée ou non scellée. Il n'y a aucune différence entre une classe scellée ou non scellée.
Le voici lors de l'appel d'une classe normale:
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 une classe scellée:
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
Pour moi, cela fournit une preuve solide qu'il ne peut y avoir aucune amélioration des performances entre les méthodes d'appel sur les classes scellées et non scellées ... Je pense que je suis content maintenant :-)