Identificando a armadilha de vazamento de memória em `collect::<Vec<_>>()` do Rust

Uma otimização em `collect::<Vec<_>>()` no Rust beta reutiliza alocações ao converter um `Vec` em outro, o que pode preservar silenciosamente grandes capacidades não usadas — especialmente ao mapear de tipos de elementos maiores para menores — levando a um uso de memória centenas de vezes maior que os dados vivos. Os comentaristas debatem se isso constitui um verdadeiro “memory leak” ou uma consequência previsível de estratégias de crescimento e reutilização, abordando conceitos como “space leaks”, o princípio da menor surpresa e como outras linguagens tratam a superalocação. Diversas correções e alternativas são sugeridas, incluindo chamadas explícitas de `shrink_to_fit`, o uso de `Box<[T]>` ou `Box<str>` para dados fixos e repensar estruturas de dados em código intensivo em memória, enquanto um relatório de bug aberto no Rust acompanha como a otimização deve ser ajustada.

Isso é realmente um “vazamento de memória”?

  • Muitos argumentam que isso não é um vazamento verdadeiro: a memória ainda pertence a Vec e é liberada quando ele é descartado; trata-se de superalocação.
  • Outros observam que, na cultura de linguagens gerenciadas, esse tipo de superalocação persistente costuma ser chamado coloquialmente de “memory leak” ou “space leak”.
  • Vários participantes enfatizam que, em Rust, “vazamento” já tem um significado preciso, então usar o termo de forma solta é enganoso, embora o impacto prático (esgotamento de RAM) ainda seja sério.

O que a nova otimização de collect::<Vec<_>>() faz

  • Rust beta reutiliza uma alocação existente de Vec ao converter via into_iter().collect(), até mesmo entre mudanças de tipo.
  • Essa reutilização não é documentada e viola o modelo mental de muitos programadores de que collect constrói um vetor novo, de tamanho razoável.
  • A intenção era economizar alocações e cópias, mas agora isso pode reter capacidades grandes e obsoletas.

Aumento surpreendente de capacidade (o caso 200x)

  • Padrão de exemplo: construir um Vec<T> grande com capacidade enorme, mapear/filtrar para menos itens de um tipo U muito menor e então fazer collect::<Vec<U>>().
  • Antes, uma nova alocação com tamanho apropriado era feita; agora o grande buffer antigo é reutilizado, então cada Vec interno mantém uma enorme capacidade não usada.
  • Em um Vec<Vec<_>> com centenas de milhares de vetores internos, isso se multiplica em gigabytes de RAM desperdiçada.
  • Alguns veem isso como um bug explícito; outros dizem que o código deveria chamar shrink_to_fit() explicitamente quando a capacidade importa.

Comportamento de memória, expectativas e padrões

  • Há um forte debate sobre o “princípio da menor surpresa”: muitos consideram reutilização entre mudanças de tipo/tamanho, especialmente quando cap >> len, algo surpreendente demais.
  • Outros enfatizam que a std/biblioteca só promete uma coleção, não um comportamento de alocação específico.
  • Uma digressão mais ampla contrasta o modelo IFNDR/UB do C++ com as semânticas mais estritas, verificadas pelo compilador, do Rust; Rust tende a favorecer rejeitar programas surpreendentes em vez de aceitá-los silenciosamente.

Superalocação no mundo real e alternativas

  • Um problema semelhante apareceu em uma implementação de Aho–Corasick: muitos Vecs pequenos com estratégia de crescimento por duplicação dobravam o pico de memória em comparação com uma implementação em C.
  • Mudar para estruturas no estilo lista encadeada (com índices) reduziu bastante o uso de memória, mostrando que Vec nem sempre é a melhor escolha.

Mitigações e tipos alternativos

  • Mitigações sugeridas: usar shrink_to_fit, Box<[T]> para arrays imutáveis, Box<str>/Arc<str> para strings, ou projetar estruturas de dados que evitem muitos Vecs expansíveis.
  • Alguns propõem heurísticas em collect (por exemplo, encolher quando a capacidade excede um múltiplo do comprimento), mas outros se preocupam com overhead oculto e complexidade.