Pile contre Tas

Lors de l’exécution d’un programme, les données doivent être stockées en mémoire. L’espace occupé et l’emplacement de stockage des données sont déterminés par leur type. Deux zones de mémoire sont utilisées pour conserver les données pendant l’exécution : la Pile (Stack) et le Tas (Heap).

La Pile (Stack)

La pile est un tableau de mémoire contigu qui suit la structure de données Dernier entré, premier sorti (LIFO). Elle stocke trois catégories de données :

  • Les valeurs de certains types de variables
  • Le contexte d’exécution actuel du programme
  • Les paramètres transmis aux méthodes
La pile ressemble à un tube à pièces : on ne peut ajouter et retirer des éléments que par une seule extrémité.

Les opérations de mémoire sur la pile sont gérées automatiquement par le système, le développeur n’a pas besoin de les manipuler manuellement. Comprendre le fonctionnement interne de la pile facilite grandement l’analyse du flux d’exécution du programme et la lecture de la documentation officielle C#.

Caractéristiques fondamentales de la pile

  • On ne peut ajouter ni supprimer des données qu’au sommet de la pile
  • Ajout de données au sommet : Push (empiler)
  • Récupération de données depuis le sommet : Pop (dépiler)

Spécificités de la mémoire de pile

  • Allocation et libération de mémoire extrêmement rapides, espace mémoire contigu
  • Cycle de vie lié à l’appel de méthode : la trame de pile est détruite automatiquement une fois la méthode terminée
  • Contenu stocké : variables de type valeur, paramètres de méthode, variables locales, contexte d’exécution

Pour mieux comprendre le fonctionnement du tas, observons un exemple de code simple

class Program
{
    static void Main(string[] args)
    {
        int a = 10;
        int b = 20;
        int sum = Add(a, b);
    }

    static int Add(int x, int y)
    {
        return x + y;
    }
}
Langage du code : JavaScript (javascript)

Cet exemple a une logique très simple. Dès la première ligne de la fonction Main, point d’entrée du programme, on crée une variable de type entier a, puis la variable b sur la seconde ligne. La troisième ligne appelle la fonction Add en transmettant les valeurs de a et b à l’intérieur de la fonction. x reçoit la valeur de a, y reçoit la valeur de b, la fonction renvoie le résultat de x + y égal à 30 vers l’appelant pour l’affecter à sum. Lorsque la fonction Add finit de s’exécuter, x et y sont dépilés, la valeur de sum devient 30, puis le programme se termine à la fin de Main. Nous détaillons ci-dessous chaque étape du processus d’empilage et de dépilage dans la pile.

Flux d’exécution du programme et processus Push/Pop de la pile

Lors de la création de la variable a, les types primitifs intégrés sont alloués directement sur la pile via une opération Push
Exécution de la seconde ligne de code, création d’une seconde variable entière b de valeur 20. Les données empilées en premier se trouvent au fond de la pile, celles ajoutées ensuite au sommet
La troisième ligne exécute d’abord Add(a,b) à droite du signe égal pour appeler la fonction Add. Les valeurs de a et b sont copiées respectivement dans x et y ; il s’agit d’un passage par valeur, a et x sont deux variables totalement indépendantes sans lien entre elles.
Cette étape ne fait que calculer x + y et renvoyer le résultat sans stockage persistant sur la pile. La valeur 30 est transmise à l’appelant comme pour l’instruction int sum = 30;. Après l’exécution de return x+y, la fonction Add s’apprête à se terminer : toutes les variables locales de la fonction sont dépilées, ce qui efface l’espace mémoire qu’elles occupaient pour une réutilisation ultérieure.
La position la plus haute de la pile s’appelle le sommet (Top) ; après un Pop, le pointeur de sommet descend.
À la fin d’Add, x et y sont des paramètres locaux de cette fonction, ils sont dépilés l’un après l’autre pour libérer de l’espace et assurer le bon fonctionnement du système.
Une fois dépilés, x et y disparaissent ; la valeur 30 renvoyée par Add est transmise à l’appelant pour être stockée dans sum.
L’instruction int sum = 30 déclenche une nouvelle opération Push : la variable sum avec la valeur 30 est placée au sommet de la pile. Une fois cette ligne achevée, Main est sur le point de se terminer, les variables locales sum, b et a seront successivement détruites par Pop.
Les opérations Pop s’exécutent toujours depuis le sommet. La pile est comparable à un couloir à une seule entrée et sortie : tous les éléments entrent et sortent par la même extrémité. C’est pourquoi l’ordre de destruction des variables dans Main est inverse de leur création : d’abord sum, puis b, enfin a.
La variable b est dépilée et détruite, le pointeur du sommet de la pile descend automatiquement.
Toutes les variables sont dépilées, y compris la dernière variable a. Strictement parlant, Main possède une autre variable locale : le tableau de chaînes args dans Main(string[] args), qui reçoit les arguments de ligne de commande saisis au lancement de la console. Ce tableau stocke ses données sur deux zones mémoire distinctes, un sujet lié au tas que nous verrons plus tard ; nous omettons args pour simplifier cette démonstration.

Retenez un point essentiel : lors de l’exécution d’une fonction, les variables sont créées de haut en bas et empilées dans l’ordre a → b → entrée dans Add → x → y. À la fin d’Add, x et y sont dépilés, puis sum est empilé ; quand Main se termine, les variables sont détruites dans l’ordre inverse de leur création : sum, b, a.


Le Tas (Heap)

Le tas est une vaste zone de mémoire dédiée à l’allocation dynamique de blocs pour stocker des objets de données. Contrairement à la pile, les données du tas peuvent être ajoutées et supprimées dans n’importe quel ordre.

Le programme ne peut pas supprimer manuellement les objets du tas ; le Ramasse-miettes GC du CLR (Common Language Runtime) nettoie automatiquement les objets orphelins sans référence inaccessible. Cela évite les erreurs liées à la libération manuelle de mémoire indispensable dans d’autres langages.

Le tas peut être comparé à un grand bureau sur lequel on peut poser des objets librement. La pile, quant à elle, ressemble au tube à pièces vu précédemment, avec une seule extrémité pour les entrées et sorties.

Le tas est comme un grand bureau sans contrainte de rangement. Le développeur n’a pas à supprimer les objets du tas manuellement, c’est le GC qui gère cela. Dès qu’aucune partie du programme ne référence plus un objet, le GC le détruit périodiquement pour libérer de la mémoire. Cela allège considérablement la charge du développeur et évite les crashs dus aux fuites de mémoire non libérées. Des langages comme C++ obligent le développeur à libérer manuellement toute mémoire du tas.

Cycle de collecte du GC

  1. Le programme crée trois objets dans le tas et conserve des références vers eux
  2. Le programme cesse d’utiliser l’un des objets, toutes ses références disparaissent
  3. Le GC détecte l’objet orphelin sans référence et libère la mémoire qu’il occupe
  4. Collecte terminée, la mémoire libérée peut être allouée à de nouveaux objets

Caractéristiques de la mémoire de tas

  • Mémoire non contiguë, vitesse d’allocation inférieure à celle de la pile
  • Cycle de vie indépendant des limites de méthode, géré de manière centralisée par le GC
  • Contenu stocké : instances de types référence (classes, tableaux, délégués…), la pile ne conserve que les adresses mémoire des objets du tas

Voici un exemple concret pour illustrer le fonctionnement. Comme vu précédemment, les variables locales de type primitif sont stockées sur la pile, tandis que les types définis par l’utilisateur résident généralement dans le tas. Voici le code correspondant :

class Program
{
    static void Main(string[] args)
    {
        int a = 10;
        int b = 20;
        Student tom = new Student("Tom", 12);
        Student lucy = new Student("Lucy", 12);
    }

}

class Student
{
    // Variables membres
    private string name;

    private int age;

    // Constructeur, détaillé plus tard
    public Student(string name, int age)
    {
        this.name = name;
        this.age = age;
    }

    // Méthode membre
    public void DisplayInfo()
    {
        Console.WriteLine($"Name: {name}, Age: {age}");
    }
}   
Langage du code : JavaScript (javascript)

L’illustration ci-dessous montre le stockage en mémoire des quatre variables a, b, tom et lucy au sein de Main. Comme pour l’explication de la pile, le code s’exécute de haut en bas : a est empilé en premier, puis b. Les valeurs de a et b sont stockées directement sur la pile.

Les variables de type primitif sont stockées directement sur la pile
Les données réelles du type personnalisé Student sont stockées dans le tas ; la pile ne conserve que l’adresse de la zone mémoire de l’objet dans le tas.
Par exemple, les données de tom se trouvent dans le bloc 00001 du tas, la pile stocke l’adresse 00001 pour retrouver les données par la suite.

Lorsque la fonction Main termine son exécution, la référence d’adresse lucy: 01010 sur la pile est dépilée et détruite. Seule l’adresse disparaît, les données de l’objet dans le tas subsistent. À ce moment-là, l’objet du tas perd toutes les références enregistrées par les variables de la pile.

L’objet Lucy du bloc 01010 du tas n’a plus aucune variable sur la pile qui enregistre son adresse, il devient un objet orphelin. Le ramasse-miettes GC le détruira à un moment opportun.
L’objet Lucy dans le tas perd toutes les références externes une fois que l’adresse stockée sur la pile est dépilée. Le GC effacera la mémoire de cet objet à tout moment pour réutiliser l’espace. Tant qu’une référence existe quelque part dans le programme, le GC ne détruit pas l’objet.

Zones de mémoire

La pile et le tas sont des divisions logiques de mémoire au niveau logiciel, pas des partitions matérielles physiques.

Pile et tas ne sont que des séparations logiques définies par le logiciel ; la mémoire RAM physique ne possède pas de modules matériels dédiés marqués « zone pile » ou « zone tas ».

La mémoire RAM physique n’est composée que de puces mémoire uniformes avec des adresses physiques continues, aucun marqueur ne distingue la pile du tas au niveau matériel

Le système d’exploitation attribue un espace de mémoire virtuelle indépendant à chaque programme en cours d’exécution, puis divise cette mémoire virtuelle en quatre segments logiques principaux :

Segment de code : Stocke les instructions compilées du programme

Segment statique global : Variables statiques, constantes

Segment de pile (Stack) : Le fil principal et les fils secondaires du programme disposent chacun d’un bloc de pile indépendant

Segment de tas (Heap) : Grand pool de mémoire partagé pour l’allocation dynamique

La pile et le tas évoqués ici ne sont que des zones logiques attribuées par l’OS à chaque processus, sans isolation matérielle entre elles.

Pile contre Tas

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *