Introduction : pourquoi profiler du code Rust ?
Rust est réputé pour sa vitesse et sa sécurité, mais même le code le plus rapide peut être lent à cause de goulots d'étranglement non évidents. Le profilage est le processus de mesure du temps d'exécution, de l'utilisation de la mémoire et d'autres ressources, qui permet de trouver les "goulots d'étranglement". Sans profilage, l'optimisation devient un jeu de devinettes : vous pouvez "améliorer" des parties de code qui n'affectent pas du tout les performances.
Dans cet article, nous allons examiner les principaux outils de profilage Rust : perf, flamegraph, cargo-flamegraph et le benchmarking intégré. Vous apprendrez à trouver les fonctions lentes, les allocations et les défauts de cache, ainsi qu'à interpréter les données obtenues.
Outils de profilage Rust : choisir la bonne approche
1. Benchmarking intégré avec #[bench]
La première étape vers le profilage est l'écriture de benchmarks. Rust prend en charge les benchmarks de manière native (instable, nécessite nightly). Exemple :
// Dans le fichier benches/my_bench.rs#![feature(test)]extern crate test;
use test::Bencher;
#[bench]fn bench_my_function(b: &mut Bencher) { let data = vec![1, 2, 3, 4, 5]; b.iter(|| { // Fonction testée let _ = data.iter().sum::<i32>(); });}Exécution : cargo bench. Vous obtiendrez des mesures de temps précises, mais cela ne montrera pas où le temps est passé à l'intérieur de la fonction.
2. perf — profileur au niveau du noyau
L'outil perf (Linux) collecte des échantillons de compteurs du processeur. Il montre quelles lignes de code sont exécutées le plus souvent.
Installation : sudo apt install linux-tools-common linux-tools-generic.
Exemple d'utilisation :
# Compilation avec symboles de débogagecargo build --release
# Lancement du profilagesudo perf record ./target/release/my_program
# Consultation du rapportsudo perf reportDans le rapport, vous verrez les fonctions triées par pourcentage de temps d'exécution. C'est le premier candidat à l'optimisation.
3. flamegraph — visualisation de la pile d'appels
Flamegraph est un diagramme SVG où la largeur de chaque rectangle correspond au temps d'exécution de la fonction. Plus le rectangle est large, plus le temps est passé dans cette fonction ou ses appels enfants.
Installation de cargo-flamegraph :
cargo install flamegraphExécution :
cargo flamegraph --bin my_programLe résultat est un fichier flamegraph.svg. Ouvrez-le dans un navigateur. Cherchez les larges "plateaux" — ce sont les goulots d'étranglement.
Exemple pratique : trouver un goulot d'étranglement dans du code Rust
Considérons un programme simple qui traite un grand tableau de données :
fn process(data: &[u8]) -> Vec<u8> { let mut result = Vec::with_capacity(data.len()); for &byte in data { result.push(byte.wrapping_mul(2)); } result}
fn main() { let data = vec![0u8; 10_000_000]; let _ = process(&data);}Compilons et lançons le profilage :
cargo build --releasecargo flamegraph --bin my_programEn ouvrant le flamegraph, nous voyons que le temps principal est passé dans la boucle for et l'appel à wrapping_mul. Notons que Vec::push peut provoquer des réallocations si la capacité n'est pas calculée avec précision. Dans notre cas, with_capacity est correctement défini, mais la boucle elle-même est le goulot d'étranglement.
Optimisation : utilisons des itérateurs et map :
fn process_optimized(data: &[u8]) -> Vec<u8> { data.iter().map(|&b| b.wrapping_mul(2)).collect()}Une nouvelle mesure montre une accélération de 10 à 15 % grâce à la vectorisation (auto-vectorisation LLVM).