Les classes scellées offrent-elles vraiment des avantages en termes de performances?

140

J'ai rencontré de nombreux conseils d'optimisation qui disent que vous devez marquer vos cours comme scellés pour obtenir des avantages supplémentaires en termes de performances.

J'ai effectué quelques tests pour vérifier le différentiel de performances et n'en ai trouvé aucun. Est-ce que je fais quelque chose de mal? Est-ce que je rate le cas où les classes scellées donneront de meilleurs résultats?

Quelqu'un a-t-il effectué des tests et vu une différence?

Aidez-moi à apprendre :)

Vaibhav
la source
9
Je ne pense pas que les classes scellées étaient destinées à augmenter les performances. Le fait qu'ils le fassent peut être fortuit. En plus de cela, profilez votre application après l'avoir refactorisée pour utiliser des classes scellées et déterminez si cela en valait la peine. Verrouiller votre extensibilité pour effectuer une micro-optimisation inutile vous coûtera à long terme. Bien sûr, si vous avez profilé, et que cela vous permet d'atteindre vos benchmarks de performance (plutôt que de perf pour des raisons de performance), vous pouvez alors prendre une décision en équipe si cela vaut l'argent dépensé. Si vous avez des classes scellées pour des raisons non liées à la performance, gardez-les :)
Merlyn Morgan-Graham
1
Avez-vous essayé avec réflexion? J'ai lu quelque part que l' instanciation par la réflexion est plus rapide avec des classes scellées
OnOF
À ma connaissance, il n'y en a pas. Sealed est là pour une raison différente - pour bloquer l'extensibilité, ce qui peut être utile / nécessaire dans de nombreux cas. L'optimisation des performances n'était pas un objectif ici.
TomTom
... mais si vous pensez au compilateur: si votre classe est scellée, vous connaissez l'adresse d'une méthode que vous appelez sur votre classe au moment de la compilation. Si votre classe n'est pas scellée, vous devez résoudre la méthode au moment de l'exécution, car vous devrez peut-être appeler une substitution. Ce sera certainement négligeable, mais je pouvais voir qu'il y aurait une différence.
Dan Puzey
Oui, mais cela ne se traduit pas par de réels avantages, comme l’a souligné le PO. Les différences architecturales sont / peuvent être beaucoup plus pertinentes.
TomTom le

Réponses:

60

Le JITter utilisera parfois des appels non virtuels à des méthodes dans des classes scellées car il n'y a aucun moyen de les étendre davantage.

Il existe des règles complexes concernant le type d'appel, virtuel / non virtuel, et je ne les connais pas toutes, donc je ne peux pas vraiment les décrire pour vous, mais si vous recherchez des classes scellées et des méthodes virtuelles sur Google, vous trouverez peut-être des articles sur le sujet.

Notez que tout type d'avantage de performance que vous obtiendriez de ce niveau d'optimisation doit être considéré comme le dernier recours, toujours optimiser au niveau algorithmique avant d'optimiser au niveau du code.

Voici un lien mentionnant ceci: Rambling sur le mot-clé scellé

Lasse V. Karlsen
la source
2
le lien «décousu» est intéressant en ce qu'il sonne comme de la bonté technique mais est en fait absurde. Lisez les commentaires sur l'article pour plus d'informations. Résumé: les 3 raisons invoquées sont la gestion des versions, les performances et la sécurité / prévisibilité - [voir le commentaire suivant]
Steven A. Lowe
1
[suite] Le versionnage ne s'applique que lorsqu'il n'y a pas de sous-classes, duh, mais étendez cet argument à chaque classe et du coup vous n'avez plus d'héritage et devinez quoi, le langage n'est plus orienté objet (mais simplement basé sur objet)! [voir suivant]
Steven A. Lowe
3
[suite] l'exemple de performance est une blague: l'optimisation d'un appel de méthode virtuelle; pourquoi une classe scellée aurait-elle une méthode virtuelle en premier lieu puisqu'elle ne peut pas être sous-classée? Enfin, l'argument Sécurité / Prévisibilité est clairement insensé: «vous ne pouvez pas l'utiliser donc c'est sécurisé / prévisible». LOL!
Steven A. Lowe
16
@Steven A. Lowe - Je pense que ce que Jeffrey Richter essayait de dire de manière légèrement détournée, c'est que si vous laissez votre classe non scellée, vous devez réfléchir à la façon dont les classes dérivées peuvent / vont l'utiliser, et si vous n'avez pas le le temps ou l'envie de le faire correctement, puis scellez-le car il est moins susceptible de provoquer des changements de rupture dans le code des autres à l'avenir. Ce n'est pas du tout absurde, c'est du bon sens.
Greg Beech
6
Une classe scellée peut avoir une méthode virtuelle car elle peut dériver d'une classe qui la déclare. Lorsque vous déclarez ensuite une variable de la classe descendante scellée et que vous appelez cette méthode, le compilateur peut émettre un appel direct à l'implémentation connue, car il sait qu'il n'y a aucun moyen que cela puisse être différent de ce qui est connu. vtable au moment de la compilation pour cette classe. Quant à scellé / non scellé, c'est une discussion différente, et je suis d'accord avec les raisons pour lesquelles les classes sont scellées par défaut.
Lasse V. Karlsen
143

La réponse est non, les classes scellées ne fonctionnent pas mieux que les classes non scellées.

Le problème se résume aux codes op callvs callvirtIL. Callest plus rapide que callvirt, et callvirtest principalement utilisé lorsque vous ne savez pas si l'objet a été sous-classé. Les gens supposent donc que si vous scellez une classe, tous les codes op changeront de calvirtsàcalls et seront plus rapides.

Malheureusement, callvirtil y a aussi d'autres choses qui le rendent utile, comme la vérification des références nulles. Cela signifie que même si une classe est scellée, la référence peut toujours être nulle et donccallvirt est nécessaire. Vous pouvez contourner cela (sans avoir besoin de sceller la classe), mais cela devient un peu inutile.

Utilisation des structures call car elles ne peuvent pas être sous- et ne sont jamais nulles.

Voir cette question pour plus d'informations:

Appel et callvirt

Cameron MacFarland
la source
5
AFAIK, les circonstances où callest utilisé sont: dans la situation new T().Method(), pour les structméthodes, pour les appels non virtuels à des virtualméthodes (comme base.Virtual()), ou pour des staticméthodes. Partout ailleurs utilise callvirt.
porges du
1
Euh ... je me rends compte que c'est vieux, mais ce n'est pas tout à fait vrai ... la grande victoire avec les classes scellées est quand le JIT Optimizer peut intégrer l'appel ... dans ce cas, la classe scellée peut être une énorme victoire .
Brian Kennedy
5
Pourquoi cette réponse est fausse. De Mono changelog: "Optimisation de la dévirtualisation pour les classes et méthodes scellées, améliorant les performances de pystone IronPython 2.0 de 4%. D'autres programmes peuvent s'attendre à une amélioration similaire [Rodrigo].". Les classes scellées peuvent améliorer les performances, mais comme toujours, cela dépend de la situation.
Smilediver
1
@Smilediver Cela peut améliorer les performances, mais seulement si vous avez un mauvais JIT (aucune idée de la qualité des JIT .NET de nos jours - c'était plutôt mauvais à cet égard). Hotspot, par exemple, intégrera les appels virtuels et se désoptimisera plus tard si nécessaire - par conséquent, vous ne payez les frais généraux supplémentaires que si vous sous-classez réellement la classe (et même alors pas nécessairement).
Voo le
1
-1 Le JIT ne doit pas nécessairement générer le même code machine pour les mêmes opcodes IL. La vérification nulle et l'appel virtuel sont des étapes orthogonales et séparées de callvirt. Dans le cas des types scellés, le compilateur JIT peut toujours optimiser une partie de callvirt. Il en va de même lorsque le compilateur JIT peut garantir qu'une référence ne sera pas nulle.
rightfold
29

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 callplace 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 WriteItmé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 :-)

Orion Edwards
la source
Une raison pour laquelle vous avez publié cette réponse deux fois au lieu de fermer l'autre comme un double? Ils ressemblent à des doublons valides pour moi bien que ce ne soit pas mon domaine.
Flexo
1
Et si les méthodes étaient plus longues (plus de 32 octets de code IL) pour éviter l'inclusion et voir quelle opération d'appel serait utilisée? S'il est en ligne, vous ne voyez pas l'appel, vous ne pouvez donc pas juger des effets.
ygoe
Je suis confus: pourquoi y a-t-il callvirtquand ces méthodes ne sont pas virtuelles pour commencer?
freakish
@freakish Je ne me souviens pas où j'ai vu cela, mais j'ai lu que le CLR utilisé callvirtpour les méthodes scellées car il doit toujours vérifier la valeur nulle de l'objet avant d'appeler la méthode, et une fois que vous en tenez compte, vous pouvez aussi bien utiliser callvirt. Pour supprimer callvirtet sauter directement, ils auraient soit besoin de modifier C # pour autoriser ((string)null).methodCall()comme le fait C ++, soit de prouver statiquement que l'objet n'était pas nul (ce qu'ils pouvaient faire, mais ne se sont pas souciés de le faire)
Orion Edwards
1
Des accessoires pour aller à l'effort de creuser jusqu'au niveau du code machine, mais soyez très prudent en faisant des déclarations comme «cela fournit une preuve solide qu'il ne peut y avoir aucune amélioration des performances». Ce que vous avez montré, c'est que pour un scénario particulier, il n'y a aucune différence dans la sortie native. C'est un point de données, et on ne peut pas supposer qu'il se généralise à tous les scénarios. Pour commencer, vos classes ne définissent aucune méthode virtuelle, donc les appels virtuels ne sont pas du tout requis.
Matt Craig
24

Comme je le sais, il n'y a aucune garantie de bénéfice de performance. Mais il y a une chance de réduire la pénalité de performance sous certaines conditions spécifiques avec une méthode scellée. (La classe scellée rend toutes les méthodes scellées.)

Mais cela dépend de l'implémentation du compilateur et de l'environnement d'exécution.


Détails

De nombreux processeurs modernes utilisent une longue structure de pipeline pour augmenter les performances. Étant donné que le processeur est incroyablement plus rapide que la mémoire, le processeur doit pré-extraire le code de la mémoire pour accélérer le pipeline. Si le code n'est pas prêt au bon moment, les pipelines seront inactifs.

Il existe un gros obstacle appelé répartition dynamique qui perturbe cette optimisation de «prélecture». Vous pouvez comprendre cela comme un simple branchement conditionnel.

// 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();

Le processeur ne peut pas pré-lire le code suivant à exécuter dans ce cas car la position de code suivante est inconnue jusqu'à ce que la condition soit résolue. Donc cela fait du danger inactif du pipeline. Et la pénalité de performance au ralenti est énorme en régulier.

Une chose similaire se produit en cas de remplacement de méthode. Le compilateur peut déterminer la substitution de méthode appropriée pour l'appel de méthode actuel, mais c'est parfois impossible. Dans ce cas, la méthode appropriée ne peut être déterminée qu'au moment de l'exécution. Il s'agit également d'un cas d'envoi dynamique, et une des principales raisons pour lesquelles les langues à typage dynamique sont généralement plus lentes que les langues à typage statique.

Certains processeurs (y compris les puces x86 d'Intel récentes) utilisent une technique appelée exécution spéculative pour utiliser le pipeline même sur la situation. Prérécupérez simplement l'un des chemins d'exécution. Mais le taux de réussite de cette technique n'est pas si élevé. Et l'échec de la spéculation entraîne un blocage du pipeline, ce qui entraîne également une énorme pénalité en termes de performances. (Ceci est entièrement par mise en œuvre du processeur. Certains processeurs mobiles sont connus car ce type d'optimisation ne permet pas d'économiser de l'énergie)

Fondamentalement, C # est un langage compilé statiquement. Mais pas toujours. Je ne connais pas la condition exacte et cela dépend entièrement de la mise en œuvre du compilateur. Certains compilateurs peuvent éliminer la possibilité de répartition dynamique en empêchant le remplacement de méthode si la méthode est marquée comme sealed. Les compilateurs stupides ne le peuvent pas. C'est l'avantage de performance du sealed.


Cette réponse ( Pourquoi est-il plus rapide de traiter un tableau trié qu'un tableau non trié? ) Décrit beaucoup mieux la prédiction de branche.

éonil
la source
1
Les processeurs de classe Pentium prélèvent directement la distribution indirecte. Parfois, la redirection du pointeur de fonction est plus rapide que si (impossible à deviner) pour cette raison.
Joshua
2
Un avantage des fonctions non virtuelles ou scellées est qu'elles peuvent être intégrées dans plus de situations.
CodesInChaos le
4

<Off-topic-rant>

Je déteste les classes scellées. Même si les avantages en termes de performances sont stupéfiants (ce dont je doute), ils détruisent le modèle orienté objet en empêchant la réutilisation par héritage. Par exemple, la classe Thread est scellée. Bien que je puisse voir que l'on pourrait souhaiter que les threads soient aussi efficaces que possible, je peux aussi imaginer des scénarios où pouvoir sous-classer Thread aurait de grands avantages. Auteurs de classes, si vous devez sceller vos classes pour des raisons de "performances", veuillez fournir au moins une interface pour que nous n'ayons pas à encapsuler et remplacer partout où nous avons besoin d'une fonctionnalité que vous avez oubliée.

Exemple: SafeThread aencapsuler la classe Thread car Thread est scellé et il n'y a pas d'interface IThread; SafeThread intercepte automatiquement les exceptions non gérées sur les threads, ce qui manque complètement à la classe Thread. [et non, les événements d'exception non gérés ne récupèrent pas les exceptions non gérées dans les threads secondaires].

</off-topic-rant>

Steven A. Lowe
la source
35
Je ne scelle pas mes cours pour des raisons de performance. Je les scelle pour des raisons de conception. Concevoir pour l'héritage est difficile, et cet effort sera gaspillé la plupart du temps. Je suis tout à fait d'accord sur le fait de fournir des interfaces - c'est une solution bien supérieure pour décoller les classes.
Jon Skeet
6
L'encapsulation est généralement une meilleure solution que l'héritage. Pour prendre votre exemple de thread spécifique, le recouvrement d'exceptions de thread rompt le principe de substitution de Liskov car vous avez modifié le comportement documenté de la classe Thread, donc même si vous pouviez en dériver, il ne serait pas raisonnable de dire que vous pourriez utiliser SafeThread partout. vous pouvez utiliser Thread. Dans ce cas, vous feriez mieux d'encapsuler Thread dans une autre classe qui a un comportement documenté différent, ce que vous pouvez faire. Parfois, les choses sont scellées pour votre bien.
Greg Beech
1
@ [Greg Beech]: opinion, pas fait - pouvoir hériter de Thread pour corriger un oubli odieux dans sa conception n'est PAS une mauvaise chose ;-) Et je pense que vous surestimez LSP - la propriété prouvable q (x) dans ce cas est «une exception non gérée détruit le programme» qui n'est pas une «propriété souhaitable» :-)
Steven A. Lowe
1
Non, mais j'ai eu ma part de code merdique où j'ai permis à d'autres développeurs d'abuser de mes trucs en ne les scellant pas ou en autorisant les cas de coin. La plupart de mon code de nos jours est constitué d'assertions et d'autres éléments liés aux contrats. Et je suis assez ouvert sur le fait que je ne fais ça que pour être pénible.
Turing a terminé
2
Puisque nous faisons des diatribes hors sujet ici, tout comme vous détestez les classes scellées, je déteste avaler les exceptions. Il n'y a rien de pire que quand quelque chose se passe mais le programme continue. JavaScript est mon préféré. Vous modifiez un code et cliquez soudainement sur un bouton ne fait absolument rien. Génial! ASP.NET et UpdatePanel en est un autre; sérieusement, si mon gestionnaire de boutons lance, c'est une grosse affaire et il doit CRASH, pour que je sache qu'il y a quelque chose à réparer! Un bouton qui ne fait rien est plus inutile qu'un bouton qui fait apparaître un écran de crash!
Roman Starkov
4

Le marquage d'une classe sealedne devrait avoir aucun impact sur les performances.

Il y a des cas où il cscpeut être nécessaire d'émettre un callvirtopcode au lieu d'un callopcode. Cependant, il semble que ces cas soient rares.

Et il me semble que le JIT devrait être capable d'émettre le même appel de fonction non virtuelle pour callvirtcelui qu'il ferait call, s'il sait que la classe n'a pas (encore) de sous-classes. Si une seule implémentation de la méthode existe, il est inutile de charger son adresse à partir d'une table virtuelle - appelez simplement l'implémentation directement. D'ailleurs, le JIT peut même intégrer la fonction.

C'est un peu un pari de la part du JIT, car si une sous - classe est chargée ultérieurement, le JIT devra jeter ce code machine et recompiler le code, émettant un véritable appel virtuel. Je suppose que cela n'arrive pas souvent dans la pratique.

(Et oui, les concepteurs de machines virtuelles poursuivent de manière agressive ces minuscules gains de performances.)

Jason Orendorff
la source
3

Les classes scellées devraient fournir une amélioration des performances. Puisqu'une classe scellée ne peut pas être dérivée, tous les membres virtuels peuvent être transformés en membres non virtuels.

Bien sûr, nous parlons de très petits gains. Je ne marquerais pas une classe comme scellée juste pour obtenir une amélioration des performances à moins que le profilage ne révèle qu'il s'agit d'un problème.

Karl Seguin
la source
Ils devraient , mais il semble que non. Si le CLR avait le concept de types de référence non nuls, alors une classe scellée serait vraiment meilleure, car le compilateur pourrait émettre à la callplace de callvirt... J'adorerais les types de référence non nuls pour de nombreuses autres raisons aussi ... soupir :-(
Orion Edwards
3

Je considère les classes «scellées» comme le cas normal et j'ai TOUJOURS une raison d'omettre le mot-clé «scellé».

Les raisons les plus importantes pour moi sont:

a) De meilleures vérifications au moment de la compilation (la conversion vers des interfaces non implémentées sera détectée au moment de la compilation, pas seulement à l'exécution)

et, principale raison:

b) L'abus de mes cours n'est pas possible de cette façon

J'aurais aimé que Microsoft fasse du «scellé» la norme, pas du «non scellé».

Turing terminé
la source
Je crois que «omettre» (omettre) devrait être «émettre» (produire)?
user2864740
2

@Vaibhav, quel type de tests avez-vous exécuté pour mesurer les performances?

Je suppose qu'il faudrait utiliser Rotor et forer dans CLI et comprendre comment une classe scellée améliorerait les performances.

SSCLI (Rotor)
SSCLI: Infrastructure de langage commun source partagée

La Common Language Infrastructure (CLI) est la norme ECMA qui décrit le cœur du .NET Framework. Le Shared Source CLI (SSCLI), également connu sous le nom de Rotor, est une archive compressée du code source d'une implémentation fonctionnelle de l'ECMA CLI et de la spécification du langage ECMA C #, des technologies au cœur de l'architecture .NET de Microsoft.

haute technologie
la source
Le test comprenait la création d'une hiérarchie de classes, avec certaines méthodes qui faisaient un travail factice (manipulation de chaînes principalement). Certaines de ces méthodes étaient virtuelles. Ils s'appelaient ici et là. Appelez ensuite ces méthodes 100, 10000 et 100000 fois ... et mesurez le temps écoulé. Ensuite, exécutez-les après avoir marqué les classes comme scellées. et mesurer à nouveau. Aucune différence entre eux.
Vaibhav
2

les classes scellées seront au moins un tout petit peu plus rapides, mais peuvent parfois être waayyy plus rapides ... si JIT Optimizer peut en ligne des appels qui auraient autrement été des appels virtuels. Donc, là où il y a des méthodes souvent appelées qui sont suffisamment petites pour être intégrées, envisagez définitivement de sceller la classe.

Cependant, la meilleure raison de sceller une classe est de dire "Je n'ai pas conçu cela pour hériter, donc je ne vais pas vous laisser brûler en supposant qu'il a été conçu pour l'être, et je ne vais pas me brûler en me retrouvant enfermé dans une implémentation parce que je vous laisse en dériver. "

Je sais que certains ici ont dit qu'ils détestent les classes scellées parce qu'ils veulent avoir l'opportunité de dériver de n'importe quoi ... mais ce n'est SOUVENT pas le choix le plus facile à maintenir ... car exposer une classe à la dérivation vous enferme dans bien plus que de ne pas tout exposer. cette. C'est similaire à dire "Je déteste les classes qui ont des membres privés ... Je ne peux souvent pas obliger la classe à faire ce que je veux parce que je n'y ai pas accès." L'encapsulation est importante ... le scellage est une forme d'encapsulation.

Brian Kennedy
la source
Le compilateur C # utilisera toujours callvirt(appel de méthode virtuelle) pour les méthodes d'instance sur les classes scellées, car il doit encore effectuer une vérification d'objet nul sur elles. En ce qui concerne l'inlining, le CLR JIT peut (et fait) des appels de méthode virtuelle en ligne pour les classes scellées et non scellées ... alors oui. Le truc de la performance est un mythe.
Orion Edwards
1

Pour vraiment les voir, vous devez analyser le cod e JIT-Compiled (le dernier).

Code C #

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();
}

Code MIL

.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 - Code compilé

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

Bien que la création des objets soit la même, l'instruction exécutée pour appeler les méthodes de la classe scellée et dérivée / de base est légèrement différente. Après avoir déplacé les données dans les registres ou la RAM (instruction mov), à l'invocation de la méthode scellée, exécutez une comparaison entre dword ptr [ecx], ecx (instruction cmp) puis appelez la méthode tandis que la classe dérivée / base exécute directement la méthode. .

Selon le rapport rédigé par Torbj¨orn Granlund, Latences d' instruction et débit pour les processeurs AMD et Intel x86 , la vitesse de l'instruction suivante dans un Intel Pentium 4 est:

  • mov : a 1 cycle comme latence et le processeur peut supporter 2,5 instructions par cycle de ce type
  • cmp : a 1 cycle comme latence et le processeur peut supporter 2 instructions par cycle de ce type

Lien : https://gmplib.org/~tege/x86-timing.pdf

Cela signifie que, idéalement , le temps nécessaire pour appeler une méthode scellée est de 2 cycles, tandis que le temps nécessaire pour invoquer une méthode de classe dérivée ou de base est de 3 cycles.

L'optimisation des compilateurs a fait la différence entre les performances d'un scellé et d'un non scellé classés si bas que l'on parle de cercles de processeurs et pour cette raison sont sans intérêt pour la plupart des applications.

Ivan Porta
la source
-10

Exécutez ce code et vous verrez que les classes scellées sont 2 fois plus rapides:

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";
    }
}

sortie: Classe scellée: 00: 00: 00.1897568 Classe non scellée: 00: 00: 00.3826678

RuslanG
la source
9
Il y a quelques problèmes avec cela. Tout d'abord, vous ne réinitialisez pas le chronomètre entre le premier et le deuxième test. Deuxièmement, la façon dont vous appelez la méthode signifie que tous les codes d'opération seront appelés et non callvirt, donc le type n'a pas d'importance.
Cameron MacFarland
1
RuslanG, vous avez oublié d'appeler watch.Reset () après avoir exécuté le premier test. o_O:]
Oui, le code ci-dessus a des bogues. Là encore, qui peut se vanter de ne jamais introduire d'erreurs dans le code? Mais cette réponse a une chose importante mieux que toutes les autres: elle essaie de mesurer l'effet en question. Et cette réponse partage également le code pour que vous tous inspectiez et votiez [en masse] contre (je me demande pourquoi le vote en masse est nécessaire?). Contrairement à d'autres réponses. Cela mérite le respect à mon avis ... Veuillez également noter que l'utilisateur est débutant, donc approche pas très accueillante non plus. Quelques correctifs simples et ce code devient utile pour nous tous. Fix + partager la version fixe, si vous osez
Roland Pihlakas
Je ne suis pas d'accord avec @RolandPihlakas. Comme vous l'avez dit, "qui peut se vanter de ne jamais introduire d'erreurs dans le code". Réponse -> Non, aucun. Mais ce n'est pas le but. Le fait est que ce sont des informations erronées qui peuvent induire en erreur un autre nouveau programmeur. Beaucoup de gens peuvent facilement manquer que le chronomètre n'a pas été réinitialisé. Ils pourraient croire que les informations de référence sont vraies. N'est-ce pas plus nocif? Quelqu'un qui cherche rapidement une réponse peut même ne pas lire ces commentaires, il verra plutôt une réponse et il / elle peut le croire.
Emran Hussain