O mistério dos 240 elementos: por que seu código em Rust de repente fica lento?
\n\nImagine a situação: você está escrevendo código de alto desempenho em Rust, tudo voa. Você testa o algoritmo em um array de 200 elementos — ótimo. Adiciona mais 40 elementos (total 240) — e o desempenho cai 2 a 3 vezes. Parece mágica? Não, é física. Mais precisamente — a arquitetura dos processadores modernos.
\n\nNo Stack Overflow, a pergunta "Why is there a large performance impact when looping over an array with 240 or more elements?" acumulou milhares de visualizações e se tornou um clássico. Em 2025, com o surgimento de novas microarquiteturas e a complexidade da hierarquia de cache, entender esse problema é crucial para qualquer desenvolvedor Rust.
\n\nNeste artigo, não vamos apenas recontar a resposta. Vamos mostrar como isso se parece em Rust, como diagnosticar o problema e, principalmente, — como evitá-lo, usando práticas modernas de 2025.
\n\nA raiz do mal: Cache Misses
\n\nO processador não trabalha diretamente com a memória RAM. Ele usa cache — uma memória ultrarrápida no chip. Os dados são carregados no cache em blocos, chamados de cache lines (geralmente 64 bytes).
\n\nA mágica do número 240 está relacionada ao tamanho do cache L1 (geralmente 32 KB para dados) e ao tamanho do elemento. Se o array for i32 (4 bytes), 240 elementos ocupam 960 bytes. Parece que cabe no L1. Mas o problema é que o algoritmo pode trabalhar com dois ou três arrays simultaneamente, ou o compilador não consegue vetorizar o código de forma eficiente, levando a cache misses.
Na pergunta original em C++, o limite era exatamente 240 elementos, porque esbarrava nas peculiaridades do prefetching e do tamanho do cache L1/L2.
\n\nComo isso se parece em Rust?
\n\nVamos simular a situação. Vamos somar os elementos de um array, mas com um "passo" que quebra o cache.
\n\nuse std::time::Instant;\n\nfn main() {\n // Array de 240 elementos\n let size = 240;\n let data: Vec<i32> = (0..size).collect();\n \n // Execução inicial\n let start = Instant::now();\n let sum: i32 = data.iter().sum();\n let duration = start.elapsed();\n println!("240 elementos (sequencial): {:?}", duration);\n \n // Agora faremos um acesso "ruim" com passo 64 (tamanho da 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 elementos (passo 64): {:?}", duration2);\n}\n\n\nVocê ficará surpreso, mas a diferença pode ser de 10 vezes! O primeiro loop usa acesso sequencial — o processador prevê e carrega as cache lines antecipadamente. O segundo — acesso aleatório com um passo grande, que expulsa os dados do cache.
\n\nPor que exatamente 240? A causa profunda
\n\nEm 2025, os processadores têm cache L1 de dados de 32 KB a 64 KB. Mas o problema não é apenas o tamanho. A questão é a associatividade do cache (set-associative cache).
\n\nImagine o cache como uma biblioteca com 64 prateleiras (sets). Em cada prateleira cabem 8 livros (ways). Quando o processador acessa a memória, ele calcula o "número da prateleira" com base no endereço. Se a prateleira já tiver 8 livros, um terá que ser removido.
\n\nPara arrays cujo tamanho é múltiplo de uma potência de dois (por exemplo, 256), todos os elementos podem cair na mesma "prateleira", causando cache conflict misses. 240 é 256 menos 16, o que quebra essa multiplicidade e... paradoxalmente, piora a situação devido às peculiaridades do prefetching.
\n\nTeste prático em Rust
\n\nuse std::time::Instant;\n\nfn bench_access(size: usize) {\n let data: Vec<i32> = (0..size as i32).collect();\n \n // Padrão "ruim": acesso a cada 16º elemento\n let mut sum = 0i32;\n let start = Instant::now();\n for i in (0..size).step_by(16)