Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Optimisation de code et profiling

Rust est conçu pour produire des binaires extrêmement rapides et proches du matériel.

Cependant, écrire du code idiomatique ne garantit pas automatiquement des performances optimales.

L’optimisation, en Rust comme ailleurs, repose sur une règle d’or : vérifier par des tests et des mesures.

Le modèle de coût de Rust et les niveaux d’optimisation

Avant de chercher à optimiser le code, il faut s’assurer que le compilateur (rustc) est configuré pour libérer tout le potentiel du langage.

  • Debug vs Release : Par défaut, cargo run compile en mode Debug (sans optimisations, avec de nombreuses vérifications à l’exécution). Les mesures de performance n’ont de sens que sur un binaire construit avec cargo build --release.
  • Abstractions à coût nul (Zero-cost abstractions) : Les itérateurs, les fermetures (closures) et les génériques sont résolus à la compilation. Un cycle for écrit avec des itérateurs génère un code assembleur aussi performant, voire plus, qu’une boucle while manuelle grâce à la vectorisation automatique.

Dans le fichier Cargo.toml, il est possible d’ajuster le comportement du mode Release :

[profile.release]
# Niveau d'optimisation maximal (0 à 3)
opt-level = 3
# Active le Link-Time Optimization (optimise à travers les crates)
lto = true
# Réduit le parallélisme de compilation pour une meilleure optimisation globale
codegen-units = 1
# Supprime le mécanisme d'unwinding des panics pour alléger le binaire
panic = "abort"

Pièges de performance courants et bonnes pratiques

L’optimisation commence par éviter les allocations mémoire inutiles sur le tas (heap) et maximiser l’utilisation de la pile (stack).

  • Allocations excessives : Cloner des données (.clone()) ou allouer dynamiquement dans des boucles critiques ralentit drastiquement le code.
  • Monomorphisation : L’utilisation excessive de génériques peut faire grossir le binaire (code bloat) et saturer le cache d’instructions du processeur. L’utilisation d’objets de traits (dyn Trait) est parfois préférable.
// ❌ À éviter : allocations répétées dans la boucle
fn traiter_donnees_inefficace(entiers: &[i32]) -> Vec<String> {
    let mut resultat = Vec::new();
    for &num in entiers {
        // Chaque itération réalloue de la mémoire sur le tas
        resultat.push(format!("Valeur: {}", num)); 
    }
    resultat
}

//  Optimisé : pré-allocation et réduction du coût
fn traiter_donnees_efficace(entiers: &[i32]) -> Vec<String> {
    // Réserve la mémoire exacte en une seule fois
    let mut resultat = Vec::with_capacity(entiers.len()); 
    for &num in entiers {
        resultat.push(format!("Valeur: {}", num));
    }
    resultat
}

Profiling et Outils de Mesure

Pour identifier les goulots d’étranglement (bottlenecks), plusieurs outils natifs ou tiers s’intègrent parfaitement avec Rust.

Micro-benchmarking avec Criterion

Criterion est la bibliothèque standard de facto pour mesurer statistiquement les performances de fonctions isolées. Elle gère automatiquement l’échauffement du CPU (warm-up) et détecte les régressions d’une exécution à l’autre.

// Exemple de fichier de benchmark (benches/mon_bench.rs)
use criterion::{criterion_group, criterion_main, Criterion};

fn bench_calcul(c: &mut Criterion) {
    c.bench_function("calcul_lourd", |b| b.iter(|| {
        // Le code à mesurer va ici
        (0..1000).sum::<i32>()
    }));
}

criterion_group!(benches, bench_calcul);
criterion_main!(benches);

Profiling d’exécution (CPU & Mémoire)

Pour analyser un programme complet en cours d’exécution, on s’appuie sur les outils système :

  • perf (Linux) et Instruments (macOS) permettent d’enregistrer l’activité du CPU et de générer des Flamegraphs (représentations visuelles des fonctions qui consomment le plus de temps CPU).
  • cargo-flamegraph est un outil Cargo simple pour générer un graphique de chaleur directement depuis le terminal.
  • DHAT (via Valgrind) et samply permettent de traquer les allocations mémoire abusives et de localiser précisément la ligne de code responsable de la pression sur le tas.
# Exemple d'utilisation de cargo-flamegraph pour profiler le binaire release
cargo flamegraph --bin mon_programme