Boucle Rust ralentie sur 240 éléments : analyse et solutions

Formateur Python en ligne pour débutants

Apprenez Python facilement sans surcharge théorique. Résolvez des tâches pratiques avec vérification automatique et écrivez du code directement dans le navigateur.

Start Course

Le mystère des 240 éléments : pourquoi votre code Rust ralentit-il soudainement ?

\n\n

Imaginez la situation : vous écrivez du code haute performance en Rust, tout fonctionne à la perfection. Vous testez votre algorithme sur un tableau de 200 éléments — impeccable. Vous ajoutez 40 éléments supplémentaires (soit 240 au total) — et les performances chutent de 2 à 3 fois. De la magie ? Non, de la physique. Plus précisément — l'architecture des processeurs modernes.

\n\n

Sur Stack Overflow, la question "Why is there a large performance impact when looping over an array with 240 or more elements?" a recueilli des milliers de vues et est devenue un classique. En 2025, avec l'arrivée de nouvelles microarchitectures et la complexification de la hiérarchie du cache, comprendre ce problème est crucial pour tout développeur Rust.

\n\n

Dans cet article, nous ne nous contenterons pas de résumer la réponse. Nous montrerons à quoi cela ressemble en Rust, comment diagnostiquer le problème et, surtout, comment l'éviter en utilisant les pratiques modernes de 2025.

\n\n

La source du problème : les défauts de cache (Cache Misses)

\n\n

Le processeur ne travaille pas directement avec la mémoire vive (RAM). Il utilise le cache — une mémoire ultra-rapide intégrée à la puce. Les données sont chargées dans le cache par blocs appelés cache lines (généralement 64 octets).

\n\n

La magie du nombre 240 est liée à la taille du cache L1 (généralement 32 Ko pour les données) et à la taille des éléments. Si le tableau est de type i32 (4 octets), 240 éléments occupent 960 octets. Cela semble tenir dans le L1. Mais le problème est que l'algorithme peut travailler avec deux ou trois tableaux simultanément, ou que le compilateur ne peut pas vectoriser efficacement le code, ce qui entraîne des défauts de cache (cache misses).

\n\n

Dans la question originale en C++, le seuil était précisément de 240 éléments, car il se heurtait aux particularités du prefetching et de la taille du cache L1/L2.

\n\n

À quoi cela ressemble-t-il en Rust ?

\n\n

Modélisons la situation. Nous allons additionner les éléments d'un tableau, mais avec un "pas" qui brise le cache.

\n\n
use std::time::Instant;\n\nfn main() {\n    // Tableau de 240 éléments\n    let size = 240;\n    let data: Vec<i32> = (0..size).collect();\n    \n    // Lancement à froid\n    let start = Instant::now();\n    let sum: i32 = data.iter().sum();\n    let duration = start.elapsed();\n    println!("240 éléments (séquentiel) : {:?}", duration);\n    \n    // Maintenant, un accès "mauvais" avec un pas de 64 (taille d'une cache line)\n    let mut sum2 = 0i32;\n    let start = Instant::now();\n    for i in (0..size).step_by(64) {\n        sum2 += data[i];\n    }\n    let duration2 = start.elapsed();\n    println!("240 éléments (pas de 64) : {:?}", duration2);\n}\n
\n\n

Vous serez surpris, mais la différence peut atteindre un facteur 10 ! La première boucle utilise un accès séquentiel — le processeur prédit et charge les cache lines à l'avance. La seconde — un accès aléatoire avec un grand pas, qui chasse les données du cache.

\n\n

Pourquoi 240 exactement ? La raison profonde

\n\n

En 2025, les processeurs ont un cache L1 de données allant de 32 Ko à 64 Ko. Mais le problème ne réside pas simplement dans la taille. Il s'agit de l'associativité du cache (set-associative cache).

\n\n

Imaginez le cache comme une bibliothèque avec 64 étagères (sets). Chaque étagère peut contenir 8 livres (ways). Lorsque le processeur accède à la mémoire, il calcule le "numéro d'étagère" à partir de l'adresse. Si l'étagère contient déjà 8 livres, il faut en jeter un.

\n\n

Pour les tableaux dont la taille est un multiple d'une puissance de deux (par exemple, 256), tous les éléments peuvent tomber sur la même "étagère", provoquant des cache conflict misses. 240, c'est 256 moins 16, ce qui brise cette multiplicité et... paradoxalement, aggrave la situation en raison des particularités du prefetching.

\n\n

Test pratique en Rust

\n\n
use std::time::Instant;\n\nfn bench_access(size: usize) {\n    let data: Vec<i32> = (0..size as i32).collect();\n    \n    // "Mauvais" motif : accès à chaque 16e élément\n    let mut sum = 0i32;\n    let start = Instant::now();\n    for i in (0..size).step_by(16)                

Blogs

Book Recommendations