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.
DebugvsRelease: Par défaut,cargo runcompile en modeDebug(sans optimisations, avec de nombreuses vérifications à l’exécution). Les mesures de performance n’ont de sens que sur un binaire construit aveccargo 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 bouclewhilemanuelle 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) etInstruments(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-flamegraphest un outil Cargo simple pour générer un graphique de chaleur directement depuis le terminal.DHAT(via Valgrind) etsamplypermettent 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