Question

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

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

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

Aide-moi à apprendre :)

Était-ce utile?

La solution

Le JITter utilisera parfois des appels non virtuels à des méthodes dans des classes scellées, car il n'est pas possible 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, je ne peux donc pas vraiment les décrire pour vous, mais si vous recherchez sur Google des classes scellées et des méthodes virtuelles, vous trouverez peut-être des articles sur le sujet.

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

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

Autres conseils

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 au call contre callvirt Codes opérationnels IL. Call est plus rapide que callvirt, et callvirt est principalement utilisé lorsque vous ne savez pas si l'objet a été sous-classé.Donc les gens supposent que si vous scellez une classe, tous les codes opérationnels changeront de calvirts à calls et sera plus rapide.

Malheureusement callvirt fait d'autres choses qui le rendent également utile, comme vérifier les références nulles.Cela signifie que même si une classe est scellée, la référence peut toujours être nulle et donc un callvirt est nécessaire.Vous pouvez contourner ce problème (sans avoir besoin de sceller la classe), mais cela devient un peu inutile.

Utilisation des structures call car ils ne peuvent pas être sous-classés et ne sont jamais nuls.

Voir cette question pour plus d'informations :

Appeler et appeler

Mise à jour:Depuis .NET Core 2.0 et .NET Desktop 4.7.1, le CLR prend désormais en charge la dévirtualisation.Il peut utiliser des méthodes dans des classes scellées et remplacer les appels virtuels par des appels directs - et il peut également le faire pour des classes non scellées s'il peut déterminer qu'il est possible de le faire en toute sécurité.

Dans un tel cas (une classe scellée que le CLR ne pourrait pas autrement détecter comme pouvant être dévirtualisée en toute sécurité), une classe scellée devrait en fait offrir une sorte d'avantage en termes de performances.

Cela dit, je ne pense pas que cela vaille la peine de s'inquiéter sauf si vous aviez déjà profilé le code et déterminé que vous étiez dans un chemin particulièrement chaud 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 Release build) émet un MSIL identique, qui est le suivant :

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é offre des avantages en termes de performances est que le compilateur sait que la classe n'est pas surchargée et peut donc utiliser call au lieu de callvirt car il n'a pas besoin de vérifier les virtuels, etc.Comme démontré ci-dessus, ce n’est pas vrai.

Ma prochaine pensée é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 sous le débogueur de Visual Studio et visualisé la sortie x86 décompilée.Dans les deux cas, le code x86 était identique, à l'exception des noms de classes et des adresses mémoire de fonctions (qui doivent bien entendu être différentes).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 l'exécution sous le débogueur l'amenait à effectuer une optimisation moins agressive ?

J'ai ensuite exécuté un exécutable de version autonome en dehors de tout environnement de débogage, et j'ai utilisé WinDBG + SOS pour intervenir une fois le programme terminé et visualiser le démontage du code x86 compilé 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 intégré le WriteIt méthode directement dans l’appelant.La chose cruciale cependant est que c'é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 constitue une preuve solide qu'il existe ne peut pas y a-t-il une amélioration des performances entre les méthodes d'appel sur les classes scellées et non scellées...Je pense que je suis heureux maintenant :-)

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

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


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 moment opportun, les pipelines seront inactifs.

Il y a un gros obstacle appelé répartition dynamique ce 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é-extraire le code suivant à exécuter dans ce cas car la position du code suivant est inconnue jusqu'à ce que la condition soit résolue.Donc ça fait danger provoque l'inactivité du pipeline.Et la pénalité de performance au ralenti est énorme en mode 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 en cours, 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 de répartition dynamique et l'une des principales raisons pour lesquelles les langages typés dynamiquement sont généralement plus lents que les langages typés statiquement.

Certains processeurs (y compris les puces x86 récentes d'Intel) utilisent une technique appelée exécution spéculative utiliser le pipeline même dans la situation.Il suffit de pré-extraire 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 provoque le blocage du pipeline, ce qui entraîne également une énorme pénalité en termes de performances.(c'est entièrement par l'implémentation du CPU.certains processeurs mobiles sont connus car ils ne proposent pas ce type d'optimisation pour é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 l'implémentation 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 en termes de performances 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.

Marquer une classe sealed ne devrait avoir aucun impact sur les performances.

Il y a des cas où csc il faudra peut-être émettre un callvirt opcode au lieu d'un call opcode.Il semble cependant que ces cas soient rares.

Et il me semble que le JIT devrait pouvoir émettre le même appel de fonction non virtuelle pour callvirt que ce serait pour call, s'il sait que la classe n'a pas (encore) de sous-classes.S'il n'existe qu'une seule implémentation de la méthode, cela ne sert à rien de charger son adresse à partir d'une table virtuelle : il suffit d'appeler directement cette implémentation.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é plus tard, le JIT devra jeter ce code machine et le compiler à nouveau, émettant un véritable appel virtuel.Je suppose que cela n'arrive pas souvent dans la pratique.

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

Cours scellés devrait apporter 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.

<débat hors sujet>

je détester classes scellées.Même si les avantages en termes de performances sont étonnants (ce dont je doute), ils détruire le modèle orienté objet en empêchant la réutilisation via l'héritage.Par exemple, la classe Thread est scellée.Même si je peux comprendre que l'on souhaite que les threads soient aussi efficaces que possible, je peux également imaginer des scénarios dans lesquels le fait de pouvoir sous-classer Thread présenterait de grands avantages.Auteurs de classe, si vous doit sceller vos cours pour des raisons de « performance », veuillez fournir une interface à tout le moins, afin que nous n'ayons pas à envelopper et remplacer partout où nous avons besoin d'une fonctionnalité que vous avez oubliée.

Exemple: Fil de sécurité J'ai dû envelopper 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 dans la classe Thread.[et non, les événements d'exception non gérés le font pas récupérer les exceptions non gérées dans les threads secondaires].

</hors sujet-rant>

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 meilleurs contrôles au moment de la compilation (la diffusion vers des interfaces non implémentées sera détectée au moment de la compilation, pas seulement au moment de 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 de « scellé » la norme, et non de « descellé ».

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

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

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

L'infrastructure linguistique commune (CLI) est la norme ECMA qui décrit le noyau du framework .NET.Le CLI source partagé (SSCLI), également connu sous le nom de Rotor, est une archive compressée du code source à une implémentation fonctionnelle de l'ECMA CLI et de la spécification du langage ECMA C #, technologies au cœur de l'architecture .NET de Microsoft.

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

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

Je sais que certains ici ont dit qu'ils détestaient les cours fermés parce qu'ils voulaient avoir l'opportunité de tirer n'importe quoi...mais ce n'est SOUVENT pas le choix le plus maintenable...car exposer une classe à la dérivation vous enferme dans bien plus que ne pas exposer tout cela.C'est comme dire "Je déteste les cours qui ont des membres privés...Souvent, je n'arrive pas à faire faire à la classe ce que je veux parce que je n'y ai pas accès." L'encapsulation est importante...le scellement est une forme d’encapsulation.

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

sortir:Classe scellée :00: 00: 00.1897568 Classe non liée:00:00:00.3826678

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top