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
Vece é 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
Vecao converter viainto_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
collectconstró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 tipoUmuito menor e então fazercollect::<Vec<U>>(). - Antes, uma nova alocação com tamanho apropriado era feita; agora o grande buffer antigo é reutilizado, então cada
Vecinterno 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
Vecnem 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 muitosVecs 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.