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 :)
.net
optimization
frameworks
performance
Vaibhav
la source
la source
Réponses:
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é
la source
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
call
vscallvirt
IL.Call
est plus rapide quecallvirt
, etcallvirt
est 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 decalvirts
àcalls
et seront plus rapides.Malheureusement,
callvirt
il 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
la source
call
est utilisé sont: dans la situationnew T().Method()
, pour lesstruct
méthodes, pour les appels non virtuels à desvirtual
méthodes (commebase.Virtual()
), ou pour desstatic
méthodes. Partout ailleurs utilisecallvirt
.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.
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:
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
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:
Vs une classe scellée:
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 :-)
la source
callvirt
quand ces méthodes ne sont pas virtuelles pour commencer?callvirt
pour 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 utilisercallvirt
. Pour supprimercallvirt
et 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)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.
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 dusealed
.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.
la source
<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 a dû encapsuler 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>
la source
Le marquage d'une classe
sealed
ne devrait avoir aucun impact sur les performances.Il y a des cas où il
csc
peut être nécessaire d'émettre uncallvirt
opcode au lieu d'uncall
opcode. 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
callvirt
celui qu'il feraitcall
, 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.)
la source
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.
la source
call
place decallvirt
... J'adorerais les types de référence non nuls pour de nombreuses autres raisons aussi ... soupir :-(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é».
la source
@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.
la source
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.
la source
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.Pour vraiment les voir, vous devez analyser le cod e JIT-Compiled (le dernier).
Code C #
Code MIL
JIT - Code compilé
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:
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.
la source
Exécutez ce code et vous verrez que les classes scellées sont 2 fois plus rapides:
sortie: Classe scellée: 00: 00: 00.1897568 Classe non scellée: 00: 00: 00.3826678
la source