Comparé au C ++, le ramasse-miettes de C # semble magique, et vous pouvez très facilement écrire du code sans vous soucier de la mémoire sous-jacente. Mais si vous vous souciez des performances, savoir comment le runtime .NET gère sa RAM peut vous aider à écrire un meilleur code.
Types de valeur et types de référence
Il existe deux types de types dans .NET, qui affectent directement la gestion de la mémoire sous-jacente.
Types de valeur sont des types primitifs avec des tailles fixes comme int
, booléen
, flotte
, double
, etc. Ils sont transmis par valeur, c'est-à-dire si vous appelez someFunction (int arg)
, l'argument est copié et envoyé comme nouvel emplacement en mémoire.
Sous le capot, les types de valeur sont (généralement) stockés sur la pile. Cela s'applique principalement aux variables locales, et il existe de nombreuses exceptions où elles seront stockées sur le tas. Mais dans tous les cas, l'emplacement en mémoire où réside le type de valeur contient la valeur réelle de cette variable.
La pile n'est qu'un emplacement spécial en mémoire, initialisé avec une valeur par défaut mais capable de s'étendre. La pile est une structure de données LIFO (Last-in, First-Out). Vous pouvez le considérer comme un compartiment: les variables sont ajoutées en haut du compartiment et, lorsqu'elles sortent de la portée, .NET atteint le compartiment et les supprime une par une jusqu'à ce qu'il atteigne le bas.
La pile est beaucoup plus rapide, mais ce n'est toujours qu'un emplacement dans la RAM, pas un emplacement spécial dans le cache du processeur (bien qu'il soit plus petit que le tas, et en tant que tel, il est très susceptible d'être chaud dans le cache, ce qui améliore les performances ).
La pile tire l'essentiel de ses performances de sa structure LIFO. Lorsque vous appelez une fonction, toutes les variables définies dans cette fonction sont ajoutées à la pile. Lorsque cette fonction retourne et que ces variables sortent de la portée, la pile efface tout ce que la fonction y met. Le runtime gère cela avec cadres de pile, qui définissent des blocs de mémoire pour différentes fonctions. Les allocations de pile sont extrêmement rapides, car il suffit d’écrire une seule valeur à la fin du cadre de la pile.
C'est aussi là que le terme "StackOverflow" provient de, qui se produit lorsqu'une fonction contient trop d'appels de méthode imbriqués et remplit toute la pile.
Types de référence, cependant, ils sont soit trop gros, n'ont pas de tailles fixes ou vivent trop longtemps pour être sur la pile. Habituellement, ceux-ci prennent la forme d'objets et de classes qui ont été instanciés, mais ils incluent également des tableaux et des chaînes, dont la taille peut varier.
Les types de référence comme les instances de classes sont souvent initialisés avec le Nouveau
mot-clé, qui crée une nouvelle instance de la classe et renvoie une référence à celle-ci. Vous pouvez le définir sur une variable locale, qui utilise en fait la pile pour stocker la référence à l'emplacement sur le tas.
Le tas peut s'étendre et se remplir jusqu'à ce que l'ordinateur manque de mémoire, ce qui le rend idéal pour stocker beaucoup de données. Cependant, il n'est pas organisé et, en C #, il doit être géré avec un garbage collector pour fonctionner correctement. Les allocations de tas sont également plus lentes que les allocations de pile, même si elles sont encore assez rapides.
Cependant, il existe un certain nombre d'exceptions à ces règles, sinon les types valeur et référence seraient appelés «types de pile» et «types de tas».
- Variables externes des fonctions lambda, variables locales de
IEnumérateur
blocs et variables locales deasynchrone
les méthodes sont toutes stockées sur le tas. - Les champs de type valeur des classes sont des variables à long terme et sont toujours stockés sur le tas. Ils sont également enveloppés dans un type de référence et sont stockés à côté de ce type de référence.
- Les champs de classe statiques sont également toujours stockés sur le tas.
- Les structures personnalisées sont des types valeur, mais elles peuvent contenir des types de référence tels que des listes et des chaînes, qui sont stockées sur le tas comme d'habitude. La création d'une copie de la structure crée une nouvelle copie et une nouvelle allocation de tous les types de référence sur le tas.
L'exception la plus notable à la règle des «types de référence se trouvant sur le tas» est l'utilisation de stackalloc
avec Envergure
, qui alloue manuellement un bloc de mémoire sur la pile pour un tableau temporaire qui sera nettoyé de la pile comme d'habitude quand il sort de la portée. Cela contourne une allocation de tas relativement coûteuse et met moins de pression sur le ramasse-miettes dans le processus. Cela peut être beaucoup plus performant, mais c'est un peu une fonctionnalité avancée, donc si vous souhaitez en savoir plus à ce sujet vous pouvez lire ce guide sur la façon de l'utiliser correctement sans provoquer d'exception StackOverflow.
Qu'est-ce que la collecte des ordures?
La pile est très organisée, mais le tas est désordonné. Sans quelque chose pour le gérer, les éléments du tas ne sont pas nettoyés automatiquement, ce qui conduit votre application à manquer de mémoire car elle n'est jamais libérée.
Bien sûr, c’est un problème, c’est pourquoi le ramasse-miettes existe. Il s'exécute sur un thread d'arrière-plan et analyse périodiquement votre application à la recherche de références qui n'existent plus sur la pile, ce qui indique que le programme a cessé de se soucier des données référencées. Le runtime .NET peut entrer et nettoyer, et déplacer la mémoire dans le processus pour rendre le tas plus organisé.
Cependant, cette magie a un coût: le ramassage des ordures est lent et coûteux. Il s'exécute sur un thread d'arrière-plan, mais il existe une période pendant laquelle l'exécution du programme doit être interrompue pour exécuter le garbage collection. C'est le compromis qui accompagne la programmation en C #; tout ce que vous pouvez faire est d'essayer de minimiser les déchets que vous créez.
Dans les langages sans garbage collector, vous devez nettoyer manuellement après vous-même, ce qui est plus rapide dans de nombreux cas mais plus ennuyeux pour le programmeur. Ainsi, dans un sens, un ramasse-miettes est comme un Roomba, qui nettoie automatiquement vos sols, mais est plus lent que de simplement se lever et passer l'aspirateur.