Retour aux leçons
Scrollytelling

Stack vs Heap

Comment Rust gère l'allocation mémoire

Deux régions de mémoire

Quand un programme s'exécute, il utilise deux régions principales de la mémoire RAM : la pile (stack) et le tas (heap). Comprendre la différence est essentiel pour écrire du code Rust performant.

  Mémoire d'un processus :
  ┌─────────────────────────┐  adresses hautes
  │        Pile (Stack)      │  ← grandit vers le bas
  │  ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼  │
  │                          │
  │    (espace libre)        │
  │                          │
  │  ▲ ▲ ▲ ▲ ▲ ▲ ▲ ▲ ▲ ▲  │
  │        Tas (Heap)        │  ← grandit vers le haut
  ├─────────────────────────┤
  │   Données statiques      │
  ├─────────────────────────┤
  │   Code du programme      │
  └─────────────────────────┘  adresses basses

La pile et le tas ont des caractéristiques très différentes en termes de vitesse, de taille et de gestion. Rust vous donne un contrôle fin sur l'endroit où vos données sont stockées.

La pile (Stack)

La pile fonctionne selon le principe LIFO (Last In, First Out) : le dernier élément empilé est le premier à être dépilé. C'est exactement comme une pile d'assiettes.

Avantages de la pile

  • - Extrêmement rapide : un simple déplacement de pointeur
  • - Allocation et désallocation en temps constant O(1)
  • - Données proches en mémoire (cache-friendly)
  • - Nettoyage automatique quand la fonction retourne

Limites de la pile

  • - Taille limitée (typiquement 1 à 8 Mo)
  • - Données de taille fixe uniquement (connue à la compilation)
  • - Durée de vie liée à la portée de la fonction
  Appels de fonctions sur la pile :

  fn main()        fn calcul()      Retour de calcul()
  ┌──────────┐     ┌──────────┐     ┌──────────┐
  │          │     │ résultat │     │          │
  │          │     │ y = 20   │     │          │
  │          │     │ x = 10   │     │          │
  ├──────────┤     ├──────────┤     ├──────────┤
  │ a = 5    │     │ a = 5    │     │ a = 5    │
  │ b = 3    │     │ b = 3    │     │ b = 3    │
  └──────────┘     └──────────┘     └──────────┘
   ↑ SP              ↑ SP             ↑ SP remonte
                   (Stack Pointer)   Frame dépilée !

Le tas (Heap)

Le tas est une région de mémoire plus grande et plus flexible. L'allocation se fait via l'allocateur du système (comme malloc en C). C'est plus lent, mais les données peuvent avoir une taille dynamique et survivre au-delà de la portée d'une fonction.

  Allocation sur le tas :

  Pile (Stack)              Tas (Heap)
  ┌──────────────┐         ┌──────────┬─────┬──────────┐
  │ ptr ─────────────────→ │ "Bonjour le monde"        │
  │ len = 18     │         ├──────────┤     │          │
  │ cap = 32     │         │ (libre)  │     │          │
  └──────────────┘         ├──────────┤     │          │
                           │ vec data ├─────┘          │
  Le pointeur, la          │ [1,2,3]  │                │
  longueur et la           └──────────┴────────────────┘
  capacité sont sur
  la pile. Les données      Les données sont sur le tas.
  sont sur le tas.          L'allocateur gère l'espace.

Avantages du tas

  • - Taille dynamique : peut grandir et rétrécir
  • - Beaucoup plus grand que la pile (limité par la RAM)
  • - Données partageables entre plusieurs parties du programme

Limites du tas

  • - Plus lent : l'allocateur doit chercher un bloc libre
  • - Fragmentation possible de la mémoire
  • - Requiert une gestion manuelle ou un GC (sauf en Rust)

Rust sur la pile

Par défaut, Rust place les données sur la pile quand leur taille est connue à la compilation. C'est le cas des types primitifs et des structures de taille fixe :

stack_types.rs
fn main() {
    // Tous ces types vivent sur la pile
    let entier: i32 = 42;           // 4 octets
    let flottant: f64 = 3.14;       // 8 octets
    let booleen: bool = true;        // 1 octet
    let caractere: char = 'R';       // 4 octets (Unicode)
    let tuple: (i32, f64) = (1, 2.0); // 12 octets
    let tableau: [i32; 5] = [1, 2, 3, 4, 5]; // 20 octets

    // Les structs de taille fixe aussi
    struct Point { x: f64, y: f64 }
    let p = Point { x: 1.0, y: 2.0 }; // 16 octets sur la pile
}
  La pile après ces déclarations :
  ┌─────────────────────────────────┐
  │ p.y       = 2.0         (f64)  │
  │ p.x       = 1.0         (f64)  │
  │ tableau   = [1,2,3,4,5] (20B)  │
  │ tuple     = (1, 2.0)    (12B)  │
  │ caractere = 'R'         (4B)   │
  │ booleen   = true        (1B)   │
  │ flottant  = 3.14        (8B)   │
  │ entier    = 42          (4B)   │
  └─────────────────────────────────┘
  ↑ Stack Pointer

  Tout est contigu en mémoire → cache CPU efficace

Ces types implémentent le trait Copy : ils sont copiés automatiquement lors d'une affectation, plutôt que déplacés (moved). La copie est rapide car les données sont petites et sur la pile.

Rust sur le tas

Quand les données ont une taille dynamique ou doivent survivre au-delà d'une portée, Rust les alloue sur le tas. Les trois types les plus courants sont :

heap_types.rs
fn main() {
    // String : texte de taille dynamique
    let mut s = String::from("bonjour");
    s.push_str(" le monde"); // peut grandir !

    // Vec<T> : tableau dynamique
    let mut v: Vec<i32> = Vec::new();
    v.push(1);
    v.push(2);
    v.push(3);  // grandit automatiquement

    // Box<T> : valeur unique sur le tas
    let b = Box::new(42);  // un i32 sur le tas
}
  Pile (Stack)              Tas (Heap)
  ┌──────────────┐
  │ b            │         ┌──────┐
  │  ptr ──────────────→   │  42  │
  ├──────────────┤         └──────┘
  │ v            │         ┌───┬───┬───┬───┐
  │  ptr ──────────────→   │ 1 │ 2 │ 3 │   │
  │  len = 3     │         └───┴───┴───┴───┘
  │  cap = 4     │          capacité = 4
  ├──────────────┤
  │ s            │         ┌─────────────────┐
  │  ptr ──────────────→   │ bonjour le monde│
  │  len = 16    │         └─────────────────┘
  │  cap = 32    │
  └──────────────┘

  Métadonnées (ptr, len, cap) sur la pile.
  Données réelles sur le tas.

Quand ces variables sortent de la portée, Rust appelle automatiquement drop() qui libère la mémoire du tas. Pas besoin de free() ni de garbage collector.

auto_drop.rs
fn creer_vecteur() -> Vec<i32> {
    let v = vec![1, 2, 3]; // alloué sur le tas
    v  // propriété transférée à l'appelant
}   // si v n'était pas retourné, drop() serait appelé ici

fn main() {
    let donnees = creer_vecteur();
    println!("{:?}", donnees);
}   // drop() libère la mémoire ici

Résumé

Rust vous donne un contrôle explicite sur l'emplacement mémoire de vos données, tout en gérant la désallocation automatiquement.

CaractéristiquePile (Stack)Tas (Heap)
VitesseTres rapidePlus lent
TailleFixe (compilation)Dynamique
Durée de viePortée de la fonctionContrôlée par ownership
Types Rusti32, f64, bool, [T; N]String, Vec<T>, Box<T>
GestionAutomatique (LIFO)Automatique (drop)

Contrairement a C ou il faut appeler free() manuellement, et contrairement a Java qui utilise un garbage collector, Rust libère la mémoire du tas au moment exact ou le propriétaire sort de la portée. C'est le meilleur des deux mondes : le contrôle du C avec la sécurité d'un langage managé.