Introduction : pourquoi la performance de Rust n'est pas de la magie, mais de la discipline
Rust est à juste titre considéré comme l'un des langages de programmation système les plus rapides. Cependant, comme dans tout autre langage, le code « brut » ne fonctionne pas toujours à la limite de ses capacités. De nombreux développeurs débutants, passant de Python ou JavaScript à Rust, s'attendent à un gain de vitesse instantané, mais oublient que l'optimisation est un processus, et non un cadeau du compilateur.
Cet article est destiné à ceux qui ont déjà écrit leurs premiers programmes en Rust et qui souhaitent comprendre comment en tirer le maximum. Nous aborderons trois axes clés : le profilage (pour savoir quoi corriger), les abstractions à coût zéro (pour ne pas payer pour ce que vous n'utilisez pas) et la gestion de la mémoire (pour éviter les goulots d'étranglement). Tous les exemples seront accompagnés de code que vous pourrez exécuter immédiatement dans votre environnement.
1. Profilage : avant d'optimiser, mesurez
La règle d'or de l'optimisation : n'optimisez pas à l'aveugle. Sans profilage, vous risquez de passer des heures à « accélérer » un code qui est déjà rapide, tandis que le véritable goulot d'étranglement reste inaperçu.
Outils de profilage
Dans l'écosystème Rust, il existe plusieurs excellents outils. Pour commencer, je recommande cargo-flamegraph — il visualise où le programme passe le plus de temps. Installation :
cargo install flamegraphLancement du profilage pour votre fichier binaire :
cargo flamegraph --bin my_appAprès exécution, vous obtiendrez un fichier SVG avec un « flame graph ». Les « barres » larges montrent les fonctions qui consomment le plus de CPU.
Exemple : trouver un goulot d'étranglement
Supposons que nous ayons une fonction qui additionne les carrés des nombres :
fn sum_of_squares(n: u64) -> u64 { let mut sum = 0; for i in 1..=n { sum += i * i; } sum}Si le profileur montre que 90 % du temps est passé dans cette fonction, nous savons où regarder. Si le goulot d'étranglement est l'entrée-sortie (par exemple, la lecture d'un fichier), l'optimisation des calculs ne donnera aucun résultat.
2. Abstractions à coût zéro : comment le compilateur aide (et gêne)
L'un des principaux atouts de Rust est le concept d'abstractions à coût zéro. Cela signifie que les constructions de haut niveau (itérateurs, closures, traits) ne doivent pas ajouter de frais généraux par rapport à l'écriture manuelle de boucles. Cependant, cela ne fonctionne que si elles sont utilisées correctement.
Itérateurs vs. boucles
De nombreux débutants écrivent ceci :
let v: Vec<i32> = (0..1000).collect();let mut result = Vec::new();for i in &v { result.push(i * 2);}Ce code peut être réécrit avec des itérateurs, et le compilateur Rust l'optimisera pour produire le même code machine :
let v: Vec<i32> = (0..1000).collect();let result: Vec<i32> = v.iter().map(|x| x * 2).collect();De plus, si vous utilisez filter, map et fold séquentiellement, le compilateur peut les « fusionner » en un seul passage — cela s'appelle la fusion de boucles. Les boucles manuelles ne permettent pas une telle optimisation.
Box<dyn Trait> vs. generics
Une autre erreur courante est l'abus des objets traits (Box<dyn SomeTrait>). Ils provoquent une répartition dynamique, ce qui ajoute des frais généraux. Si le nombre de variantes est connu à la compilation, utilisez les generics (répartition statique) :
// Mauvais : répartition dynamiquefn process(items: Vec<Box<dyn Processor>>) { for item in items { item.run(); }}
// Bon : répartition statiquefn process<T: Processor>(items: Vec<T>) { for item in items { item.run()