The Mystery of 240 Elements: Why Does Your Rust Code Suddenly Slow Down?
\n\nImagine this: you're writing high-performance code in Rust, everything is blazing fast. You test your algorithm on an array of 200 elements — great. You add another 40 elements (240 total) — and performance drops by a factor of 2-3. Sounds like magic? No, it's physics. More precisely — modern CPU architecture.
\n\nThe question on Stack Overflow "Why is there a large performance impact when looping over an array with 240 or more elements?" has gathered thousands of views and become a classic. In 2025, with the emergence of new microarchitectures and the increasing complexity of the cache hierarchy, understanding this problem is critically important for any Rust developer.
\n\nIn this article, we won't just retell the answer. We'll show how this looks in Rust, how to diagnose the problem, and most importantly — how to avoid it using modern practices of 2025.
\n\nThe Root of Evil: Cache Misses
\n\nThe CPU does not work directly with RAM. It uses a cache — ultra-fast memory on the chip. Data is loaded into the cache in blocks called cache lines (typically 64 bytes).
\n\nThe magic of the number 240 is related to the size of the L1 cache (usually 32 KB for data) and the element size. If the array is i32 (4 bytes), then 240 elements occupy 960 bytes. This would seem to fit in L1. But the problem is that the algorithm might work with two or three arrays simultaneously, or the compiler cannot effectively vectorize the code, leading to cache misses.
In the original C++ question, the threshold was exactly 240 elements because it hit the limits of prefetching and the L1/L2 cache size.
\n\nHow Does This Look in Rust?
\n\nLet's simulate the situation. We'll sum the elements of an array, but with a "step" that breaks the cache.
\n\nuse std::time::Instant;\n\nfn main() {\n // Array of 240 elements\n let size = 240;\n let data: Vec<i32> = (0..size).collect();\n \n // Cold start\n let start = Instant::now();\n let sum: i32 = data.iter().sum();\n let duration = start.elapsed();\n println!("240 elements (sequential): {:?}", duration);\n \n // Now let's do "bad" access with a step of 64 (cache line size)\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 elements (step 64): {:?}", duration2);\n}\n\n\nYou'll be surprised, but the difference can be 10x! The first loop uses sequential access — the CPU predicts and loads cache lines in advance. The second uses random access with a large step, which evicts data from the cache.
\n\nWhy Exactly 240? The Deep Reason
\n\nIn 2025, processors have L1 data caches from 32 KB to 64 KB. But the problem isn't just about size. It's about cache associativity (set-associative cache).
\n\nImagine the cache as a library with 64 shelves (sets). Each shelf can hold 8 books (ways). When the CPU accesses memory, it calculates the "shelf number" from the address. If the shelf already has 8 books, one must be evicted.
\n\nFor arrays whose size is a power of two (e.g., 256), all elements can fall on the same "shelf," causing cache conflict misses. 240 is 256 minus 16, which breaks this alignment and... paradoxically makes the situation even worse due to prefetching characteristics.
\n\nPractical Test in Rust
\n\nuse std::time::Instant;\n\nfn bench_access(size: usize) {\n let data: Vec<i32> = (0..size as i32).collect();\n \n // "Bad" pattern: access every 16th element\n let mut sum = 0i32;\n let start = Instant::now();\n for i in (0..size).step_by(16)