¿Por qué un bucle sobre un array de 240 elementos se ralentiza en Rust? Análisis

Entrenador de Python en Línea para Principiantes

Aprende Python fácilmente sin sobrecargarte de teoría. Resuelve tareas prácticas con verificación automática, recibe pistas y escribe código directamente en el navegador, sin instalar nada.

Start Course

El misterio de los 240 elementos: ¿por qué tu código en Rust se ralentiza de repente?

\n\n

Imagina la situación: escribes código de alto rendimiento en Rust, todo vuela. Pruebas el algoritmo con un array de 200 elementos — perfecto. Añades otros 40 elementos (total 240) — y el rendimiento cae 2-3 veces. ¿Suena a magia? No, es física. Más concretamente: la arquitectura de los procesadores modernos.

\n\n

En Stack Overflow, la pregunta "Why is there a large performance impact when looping over an array with 240 or more elements?" acumuló miles de visitas y se convirtió en un clásico. En 2025, con la aparición de nuevas microarquitecturas y la creciente complejidad de la jerarquía de caché, entender este problema es fundamental para cualquier desarrollador de Rust.

\n\n

En este artículo no solo resumiremos la respuesta. Mostraremos cómo se manifiesta en Rust, cómo diagnosticar el problema y, lo más importante, cómo evitarlo utilizando las prácticas modernas de 2025.

\n\n

La raíz del mal: fallos de caché (Cache Misses)

\n\n

El procesador no trabaja directamente con la memoria RAM. Utiliza la caché, una memoria ultrarrápida en el chip. Los datos se cargan en la caché en bloques llamados cache lines (generalmente de 64 bytes).

\n\n

La magia del número 240 está relacionada con el tamaño de la caché L1 (normalmente 32 KB para datos) y el tamaño del elemento. Si el array es de i32 (4 bytes), 240 elementos ocupan 960 bytes. Parecería que cabe en L1. Pero el problema es que el algoritmo puede trabajar con dos o tres arrays simultáneamente, o el compilador no puede vectorizar el código de manera eficiente, lo que provoca fallos de caché (cache misses).

\n\n

En la pregunta original en C++, el umbral era exactamente 240 elementos porque se topaba con las características de la prebúsqueda (prefetching) y el tamaño de la caché L1/L2.

\n\n

¿Cómo se ve esto en Rust?

\n\n

Vamos a simular la situación. Sumaremos elementos de un array, pero con un "paso" que rompe la caché.

\n\n
use 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    // Ejecución en frío\n    let start = Instant::now();\n    let sum: i32 = data.iter().sum();\n    let duration = start.elapsed();\n    println!("240 elementos (secuencial): {:?}", duration);\n    \n    // Ahora haremos un acceso "malo" con paso de 64 (tamaño de 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 (paso 64): {:?}", duration2);\n}\n
\n\n

Te sorprenderá, ¡pero la diferencia puede ser de hasta 10 veces! El primer bucle utiliza acceso secuencial — el procesador predice y carga las cache lines por adelantado. El segundo es un acceso aleatorio con un paso grande, que expulsa datos de la caché.

\n\n

¿Por qué exactamente 240? La causa profunda

\n\n

En 2025, los procesadores tienen una caché L1 de datos de 32 KB a 64 KB. Pero el problema no es solo el tamaño. Se trata de la asociatividad de la caché (set-associative cache).

\n\n

Imagina la caché como una biblioteca con 64 estantes (sets). En cada estante caben 8 libros (ways). Cuando el procesador accede a la memoria, calcula el "número de estante" según la dirección. Si el estante ya tiene 8 libros, tendrá que tirar uno.

\n\n

Para arrays cuyo tamaño es múltiplo de una potencia de dos (por ejemplo, 256), todos los elementos pueden caer en el mismo "estante", provocando cache conflict misses. 240 es 256 menos 16, lo que rompe esa multiplicidad y... paradójicamente, empeora la situación debido a las características de la prebúsqueda.

\n\n

Prueba práctica 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    // Patrón "malo": acceso a cada 16º elemento\n    let mut sum = 0i32;\n    let start = Instant::now();\n    for i in (0..size).step_by(16)                

Blogs

Book Recommendations