Identificando el error de memoria de `collect::<Vec<_>>()` en Rust

Una optimización en `collect::<Vec<_>>()` de Rust beta reutiliza asignaciones al convertir un `Vec` en otro, lo que puede conservar silenciosamente grandes capacidades no usadas, especialmente al pasar de tipos de elemento más grandes a más pequeños, llevando el uso de memoria a niveles cientos de veces superiores a los datos vivos. Los comentaristas debaten si esto constituye una verdadera “fuga de memoria” o una consecuencia previsible de las estrategias de crecimiento y reutilización, abordando conceptos como las “space leaks”, el principio de la menor sorpresa y cómo otros lenguajes manejan la sobreasignación. Se sugieren varias soluciones y alternativas, incluyendo llamadas explícitas a `shrink_to_fit`, usar `Box<[T]>` o `Box<str>` para datos fijos, y replantear las estructuras de datos en código con uso intensivo de memoria, mientras un informe de error abierto en Rust sigue evaluando cómo debe ajustarse la optimización.

¿Esto es realmente una “fuga de memoria”?

  • Muchos sostienen que esto no es una fuga real: la memoria sigue siendo propiedad de Vec y se libera al descartarlo; es una sobreasignación.
  • Otros señalan que, en la cultura de los lenguajes gestionados, este tipo de sobreasignación persistente a menudo se llama coloquialmente “fuga de memoria” o “space leak”.
  • Varios participantes enfatizan que en Rust, “fuga” ya tiene un significado preciso, así que usarlo de forma imprecisa resulta engañoso, aunque el impacto práctico (agotamiento de RAM) sigue siendo grave.

Qué hace la nueva optimización de collect::<Vec<_>>()

  • Rust beta reutiliza una asignación de Vec existente al convertir mediante into_iter().collect(), incluso a través de cambios de tipo.
  • Esta reutilización no está documentada y viola el modelo mental de muchos programadores de que collect construye un vector nuevo y de tamaño razonable.
  • La intención era ahorrar asignaciones y copias, pero ahora puede conservar capacidades grandes y obsoletas.

Sorprendente explosión de capacidad (el caso 200x)

  • Patrón de ejemplo: construir un Vec<T> grande con una capacidad enorme, aplicar map/filter para obtener menos elementos de un tipo mucho más pequeño U, y luego collect::<Vec<U>>().
  • Antes se hacía una nueva asignación con el tamaño adecuado; ahora se reutiliza el viejo buffer grande, por lo que cada Vec interno conserva una capacidad enorme sin usar.
  • En un Vec<Vec<_>> con cientos de miles de vectores internos, esto se multiplica hasta consumir gigabytes de RAM desperdiciados.
  • Algunos lo ven como un error directo; otros dicen que el código debería llamar explícitamente a shrink_to_fit() cuando la capacidad importa.

Comportamiento de memoria, expectativas y estándares

  • Hay un fuerte debate sobre el “principio de la menor sorpresa”: muchos encuentran demasiado sorprendente la reutilización a través de cambios de tipo/tamaño, especialmente cuando cap >> len.
  • Otros subrayan que la biblioteca/estándar solo promete una colección, no un comportamiento de asignación específico.
  • Un desvío más amplio contrasta el modelo IFNDR/UB de C++ con las semánticas más estrictas verificadas por el compilador en Rust; Rust tiende a favorecer rechazar programas sorprendentes antes que aceptarlos silenciosamente.

Sobreasignación en el mundo real y alternativas

  • Un problema similar apareció en una implementación de Aho–Corasick: muchos Vec pequeños con estrategia de crecimiento por duplicación duplicaron el pico de memoria frente a una implementación en C.
  • Cambiar a estructuras estilo lista enlazada (con índices) redujo significativamente la memoria, mostrando que Vec no siempre es la mejor opción.

Mitigaciones y tipos alternativos

  • Mitigaciones sugeridas: usar shrink_to_fit, Box<[T]> para matrices inmutables, Box<str>/Arc<str> para cadenas, o diseñar estructuras de datos que eviten muchos Vec redimensionables.
  • Algunos proponen heurísticas en collect (por ejemplo, reducir cuando la capacidad supere un múltiplo de la longitud), pero otros se preocupan por el coste oculto y la complejidad.