.net - Les cours scellés offrent-ils vraiment des avantages en termes de performances?

Translate

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

This question and all comments follow the "Attribution Required."

Toutes les réponses

Translate

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, je ne peux donc pas vraiment vous les décrire, 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:Randonnée sur le mot-clé scellé

La source
Translate

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 à lacallcontrecallvirtCodes d'opération IL.Callest plus rapide quecallvirt, etcallvirtest 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àcallset sera plus rapide.

Malheureusementcallvirtfait 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 donccallvirtest nécessaire. Vous pouvez contourner cela (sans avoir besoin de sceller la classe), mais cela devient un peu inutile.

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

Voir cette question pour plus d'informations:

Appel et callvirt

La source
Translate

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 que ça vaudrait la peine de s'inquiétersauf sivous aviez 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 utilisercallau lieu decallvirtcar 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 consulté 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 intégré leWriteItmé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 que làne peux pasêtre 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 :-)

La source
Translate

Comme je le sais, il n'y a aucune garantie de performance. Mais il y aune chance de réduire la pénalité de performance dans certaines conditions spécifiquesavec 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. Parce 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 y a un gros obstacle appeléenvoi dynamiquece 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 faitdangerprovoque l'inactivité du pipeline. Et la pénalité de performance au ralenti est énorme en régulier.

Une chose similaire se produit en cas de substitution 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éeexécution spéculativepour 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 dû à la 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 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 commesealed. 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
Translate

<off-topic-rant>

I détesterclasses scellées. Même si les avantages en termes de performances sont stupéfiants (ce dont je doute), ilsdétruirele 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 classe, si vousdoitscellez vos cours pour des raisons de "performance",veuillez fournir une interfaceà tout le moins pour ne pas avoir à emballer et remplacer partout où nous avons besoin d'une fonctionnalité que vous avez oubliée.

Exemple:SafeThreada 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 à la classe Thread. [et non, les événements d'exception non gérés fontne pasramasser les exceptions non gérées dans les threads secondaires].

</off-topic-rant>

La source
Translate

Marquer une classesealedne devrait avoir aucun impact sur les performances.

Il y a des cas oùcscpourrait avoir à émettre uncallvirtopcode au lieu d'uncallopcode. Cependant, il semble que ces cas soient rares.

Et il me semble que le JIT devrait pouvoir émettre le même appel de fonction non virtuelle pourcallvirtque ce serait pourcall, 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-classeisplus tard chargé, 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
Translate

Classes scelléesdevraitfournir 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
Translate

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 en 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 "descellé".

La source
Translate

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

Je suppose qu'il faudrait utiliserRotoret pour explorer 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.

La source
Translate

les classes scellées seront au moins un tout petit peu plus rapides, mais peuvent parfois être waayyy plus rapides ... si JIT Optimizer peut intégrer 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 pour 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 la possibilité de dériver de quoi que ce soit ... 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 scellement est une forme d'encapsulation.

La source
Translate

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

La source