Pourquoi Rust nécessite-t-il une approche particulière du benchmarking ?
Rust est réputé pour sa vitesse, mais même le langage le plus rapide peut être ralenti par une implémentation maladroite. Avez-vous déjà écrit un algorithme en Rust en pensant : « Ça va être fulgurant ! », puis lancé sans comprendre où se perdaient les microsecondes ? Cela vous parle ?
Le benchmarking en Rust ne se résume pas à lancer un chronomètre et à mesurer le temps. C'est toute une science : il faut tenir compte des optimisations du compilateur, de la mise en cache du processeur, et même de la façon dont Rust gère la mémoire. Dans cet article, nous allons voir comment benchmarker correctement des programmes en Rust pour obtenir des chiffres honnêtes, et non des résultats aléatoires.
Nous passerons des mesures simples avec std::time à l'outil professionnel criterion, utilisé dans des projets réels. C'est parti !
1. La méthode la plus simple : std::time::Instant
Si vous avez besoin de « tâter » rapidement les performances d'un morceau de code, sans installer de crates, insérez simplement la mesure directement dans main. Mais attention : l'optimiseur de Rust peut « jeter » votre code s'il n'utilise pas le résultat.
use std::time::Instant;
fn main() { let start = Instant::now();
// Votre code à mesurer let mut sum = 0u64; for i in 0..1_000_000 { sum += i; }
let duration = start.elapsed(); println!("Temps d'exécution : {:?}", duration); // Utilisez impérativement le résultat ! println!("Somme : {}", sum);}Pourquoi ne pas simplement mesurer une seule exécution ?
Une seule exécution, c'est une loterie. Le système peut être occupé, le processeur peut throttler, le cache est froid. Un bon benchmark en Rust doit :
- Réchauffer le code (warm-up) — plusieurs itérations avant la mesure
- Exécuter le code de nombreuses fois (des milliers d'itérations)
- Moyenner les résultats
Implémenter cela manuellement est une source d'erreurs. C'est pourquoi la communauté Rust a créé criterion.
2. Benchmarking professionnel : le crate Criterion
Criterion.rs est la norme de facto pour le benchmarking en Rust. Il est utilisé dans le compilateur rustc lui-même et dans des centaines de crates. Pourquoi est-il génial ?
- Réchauffement automatique et traitement statistique
- Graphiques et comparaison avec les exécutions précédentes
- Détection des régressions (si le code devient plus lent, vous le saurez)
- Prise en charge des benchmarks paramétrés
Installation et configuration
Ajoutez dans Cargo.toml :
[dev-dependencies]criterion = { version = "0.5", features = ["html_reports"] }
[[bench]]name = "my_benchmark"harness = falseCréez le fichier benches/my_benchmark.rs :
use criterion::{black_box, criterion_group, criterion_main, Criterion};
// Fonction à benchmarkerfn fibonacci(n: u64) -> u64 { match n { 0 => 0, 1 => 1, _ => fibonacci(n - 1) + fibonacci(n - 2), }}
fn bench_fibonacci(c: &mut Criterion) { c.bench_function("fibonacci 20", |b| { // black_box — empêche l'optimiseur de jeter le code b.iter(|| fibonacci(black_box(20))) });}
criterion_group!(benches, bench_fibonacci);criterion_main!(benches);Lancement : cargo bench. Vous verrez quelque chose comme :
fibonacci 20 time: [24.567 µs 24.789 µs 25.012 µs]Les trois nombres représentent l'intervalle de confiance (borne inférieure, moyenne, borne supérieure). Si l'intervalle est étroit, le benchmark est stable.
3. L'ingrédient secret : black_box
Vous avez peut-être remarqué black_box dans l'exemple. C'est une fonction magique de criterion qui empêche LLVM d'optimiser les arguments ou le résultat. Sans elle, votre benchmark pourrait afficher 0 ns — parce que le compilateur aurait simplement calculé la constante à la compilation.
Exemple p