Le mystère des 240 éléments : pourquoi votre code Rust ralentit-il soudainement ?
\n\nImaginez 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\nSur 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\nDans 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\nLa source du problème : les défauts de cache (Cache Misses)
\n\nLe 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\nLa 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).
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\nModélisons la situation. Nous allons additionner les éléments d'un tableau, mais avec un "pas" qui brise le cache.
\n\nuse 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\nVous 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\nPourquoi 240 exactement ? La raison profonde
\n\nEn 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\nImaginez 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\nPour 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\nTest pratique en Rust
\n\nuse 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)