Introduction
Rust est connu pour sa vitesse et sa sécurité, mais même dans ce langage, on peut écrire du code non optimal. L'optimisation des performances des applications Rust est un art qui combine la compréhension des mécanismes bas niveau du langage, la gestion de la mémoire et l'utilisation efficace du parallélisme. Dans cet article, nous analyserons les techniques clés qui vous aideront à tirer le maximum de votre code. Vous apprendrez à profiler les applications, à utiliser les zero-cost abstractions, à gérer la mémoire sans garbage collector et à appliquer correctement le multithreading.
1. Profilage et benchmarking
Avant d'optimiser, il faut comprendre ce qui ralentit. Rust fournit d'excellents outils pour mesurer les performances.
1.1. Benchmarks intégrés (cargo bench)
Utilisez cargo bench pour écrire des micro-benchmarks. Pour cela, ajoutez dans Cargo.toml :
[dev-dependencies]criterion = { version = "0.5", features = ["html_reports"] }Exemple de benchmark :
use criterion::{black_box, criterion_group, criterion_main, Criterion};
fn fibonacci(n: u64) -> u64 { match n { 0 => 1, 1 => 1, n => fibonacci(n-1) + fibonacci(n-2), }}
fn bench_fib(c: &mut Criterion) { c.bench_function("fib 20", |b| b.iter(|| fibonacci(black_box(20))));}
criterion_group!(benches, bench_fib);criterion_main!(benches);1.2. Profilage avec perf et flamegraph
Pour les sections de code critiques, utilisez perf (Linux) ou flamegraph. Installez :
cargo install flamegraphLancez le profilage :
cargo flamegraph --bin my_appCela créera un fichier SVG avec un flamegraph, où les fonctions les plus gourmandes en ressources sont immédiatement visibles.
2. Optimisation de la gestion de la mémoire
Rust n'a pas de garbage collector, mais cela ne signifie pas que la mémoire peut être gérée négligemment. Points clés :
2.1. Évitez les allocations inutiles
Chaque appel à Box::new() ou Vec::push() peut être coûteux. Utilisez Vec::with_capacity() pour une allocation préalable de la mémoire :
// Mauvaislet mut v = Vec::new();for i in 0..1000 { v.push(i);}
// Bonlet mut v = Vec::with_capacity(1000);for i in 0..1000 { v.push(i);}2.2. Utilisez la pile plutôt que le tas
Si la taille des données est connue à la compilation, stockez-les sur la pile. Par exemple, utilisez des tableaux plutôt que Vec :
// Sur la pilelet arr: [i32; 5] = [1, 2, 3, 4, 5];
// Dans le tas (allocation inutile)let vec = vec![1, 2, 3, 4, 5];2.3. Clonage vs emprunt
Évitez les clone() coûteux. Utilisez les références et l'emprunt :
fn process(data: &[u8]) { /* ... */ }
// Mauvais : copie des donnéeslet data = vec![1, 2, 3];process(&data.clone());
// Bon : passage d'une référenceprocess(&data);3. Zero-cost abstractions et itérateurs
Rust est réputé pour le fait que les constructions de haut niveau se compilent en code machine efficace. Cependant, il est important de savoir comment les utiliser correctement.
3.1. Les itérateurs sont rapides
Les chaînes d'itérateurs fonctionnent souvent plus vite que les boucles manuelles, car le compilateur peut les optimiser (loop unrolling, inlining) :
// Boucle manuellelet mut sum = 0;for i in 0..1000 { sum += i * 2;}
// Itérateur (souvent plus rapide)let sum: i32 = (0..1000).map(|x| x * 2).sum();3.2. Évitez la dispatch dynamique
L'utilisation de dyn Trait ajoute des frais généraux liés aux appels virtuels. Dans la mesure du possible, utilisez la dispatch statique via les génériques (impl Trait) :
// Dispatch dynamique (plus lent)fn process(d: &dyn SomeTrait) { d.do_something(