Identifying Rust's collect:<Vec<_>>() memory leak footgun

An optimization in Rust’s `collect::<Vec<_>>()` on beta reuses allocations when converting one `Vec` into another, which can silently preserve large unused capacities—especially when mapping from larger element types to smaller ones—leading to memory usage hundreds of times higher than the live data. Commenters debate whether this constitutes a true “memory leak” or a predictable consequence of growth and reuse strategies, touching on concepts like “space leaks,” the principle of least astonishment, and how other languages handle over-allocation. Several remedies and alternatives are suggested, including explicit `shrink_to_fit` calls, using `Box<[T]>` or `Box<str>` for fixed data, and rethinking data structures in memory-intensive code, while an open Rust bug report tracks how the optimization should be adjusted.

Is this really a “memory leak”?

  • Many argue this is not a true leak: memory is still owned by Vec and freed when dropped; it’s over-allocation.
  • Others note that in managed-language culture this sort of persistent over-allocation is often colloquially called a “memory leak” or “space leak.”
  • Several posters stress that in Rust, “leak” already has a precise meaning, so using it loosely is misleading, though the practical impact (RAM exhaustion) is still serious.

What the new collect::<Vec<_>>() optimization does

  • Beta Rust reuses an existing Vec allocation when converting via into_iter().collect(), even across type changes.
  • This reuse is undocumented and violates many programmers’ mental model that collect constructs a fresh, reasonably-sized vector.
  • The intent was to save allocations and copying, but it can now retain large, obsolete capacities.

Surprising capacity blow‑up (the 200x case)

  • Example pattern: build large Vec<T> with huge capacity, map/filter to fewer items of a much smaller type U, then collect::<Vec<U>>().
  • Previously, a new appropriately-sized allocation was made; now the old large buffer is reused, so each inner Vec keeps massive unused capacity.
  • In a Vec<Vec<_>> with hundreds of thousands of inner vectors, this multiplies into gigabytes of wasted RAM.
  • Some see this as an outright bug; others say code should explicitly call shrink_to_fit() when capacity matters.

Memory behavior, expectations, and standards

  • Strong debate over “principle of least astonishment”: many find reuse across type/size changes, especially when cap >> len, too surprising.
  • Others emphasize that the standard/library only promises a collection, not specific allocation behavior.
  • Broader tangent contrasts C++’s IFNDR/UB model with Rust’s stricter, compiler-checked semantics; Rust tends to favor rejecting surprising programs over silently accepting them.

Real-world over-allocation and alternatives

  • A similar issue appeared in an Aho–Corasick implementation: many small Vecs with growth-by-doubling strategy doubled peak memory vs. a C implementation.
  • Switching to linked-list–style structures (with indices) cut memory substantially, showing Vec isn’t always the right choice.

Mitigations and alternative types

  • Suggested mitigations: use shrink_to_fit, Box<[T]> for immutable arrays, Box<str>/Arc<str> for strings, or design data structures that avoid many growable Vecs.
  • Some propose heuristics in collect (e.g., shrink when capacity exceeds a multiple of length), but others worry about hidden overhead and complexity.