Diretiva Using pode ferir o throughput | Artigo .NET
- Diretiva Using pode ferir o throughput | Artigo .NET
Quando o using “inocente” pode ferir o throughput da sua aplicação .NET
O bloco using em C# é uma das construções mais comuns e recomendadas para liberar recursos corretamente. Ele garante que objetos que implementam IDisposable chamem .Dispose() de forma determinista no final do escopo, evitando vazamentos de recursos e melhorando a gestão de recursos. (LinkedIn)
No entanto, existe um cenário em que esse padrão aparentemente correto pode reduzir o throughput de aplicações de alto desempenho — especialmente em caminhos críticos de código que são executados milhares de vezes por segundo. (Medium)
O problema principal
No post de Mohammad Shoeb, o autor aponta que o uso repetido de using em loops críticos — como por exemplo:
using var stream = new MemoryStream();
que parece limpo e responsável, pode criar alocações frequentes e pressão no Garbage Collector (GC) quando esse padrão é usado muitas vezes por segundo em caminhos quentes (hot paths). (Medium)
Em cenários de alta carga, esse padrão pode invisivelmente:
- Criar muitas instâncias de objetos descartáveis para cada requisição;
- Forçar o GC a trabalhar mais, pois cada instância entra nas gerações de heap do .NET;
- Reduzir o throughput global, porque o custo de alocação e descarte repetidos se torna significativo. (Medium)
Ou seja, embora using chame .Dispose() corretamente, ele não significa que a operação é barata: a alocação de objetos e seus buffers pode custar CPU e memória, e essas operações somadas em alto volume fazem diferença em aplicações que precisam escalar. (Medium)
Por que isso acontece?
Para entender o motivo, precisamos recordar como o .NET gerencia memória:
- O Garbage Collector organiza objetos em heaps (gerações Gen0, Gen1, Gen2);
- Objetos pequenos criados em grande número em um curto intervalo geralmente são coletados rapidamente, mas ainda assim geram trabalho para o GC;
- Quando muitas alocações ocorrem repetidamente — mesmo se liberadas com
using— o GC pode disparar com mais frequência para limpar essas gerações. (note.wcoder.com)
Logo, o problema não está no using em si, nem no fato de chamar .Dispose(), mas sim no custo das alocações e desalocações repetidas em caminhos de alto desempenho. (Medium)
Por que isso impacta throughput?
Throughput é uma medida de quanto trabalho um sistema consegue completar em um determinado período (como requisições por segundo). Em cenários de servidor web ou processamento em lote, throughput alto significa que você pode servir mais clientes ou processar mais dados com menos recursos.
Quando você usa estruturas que:
- Criam muitos objetos descartáveis em loops;
- Executam descarte imediato (
Dispose) repetidamente; - Forçam o GC a entrar em ação com mais frequência;
o sistema pode gastar mais tempo gerenciando memória do que executando a lógica de negócio. Isso reduz o throughput geral em cargas altas. (Medium)
Quando o using é realmente problemático
Não há nada “errado” com o using como conceito — o problema aparece quando:
- Ele é usado em caminhos críticos de desempenho, como loops intensivos;
- Os objetos descartáveis não têm recursos nativos significativos, e a sobrecarga de alocação é maior do que os benefícios práticos;
- O padrão leva a alocações altas de memória e muitos objetos temporários, mesmo sem necessidade real de liberar recursos não gerenciados.
Um exemplo típico citado:
using var stream = new MemoryStream();
Embora MemoryStream implemente IDisposable, ele não possui recursos não gerenciados substanciais — é basicamente um buffer de memória que será coletado pelo GC. Chamá-lo repetidamente pode gerar trabalho extra no GC sem muito benefício real. (Stack Overflow)
O que fazer em vez disso?
-
Reusar instâncias quando possível Em vez de criar e descartar objetos em cada iteração, considere reutilizar instâncias ou pools de objetos para reduzir alocações repetidas.
-
Evitar
usingem hot paths Se o objeto não estiver sustentando recursos nativos (como um objeto de buffer gerenciado), e a lógica permitir, evite o blocousingrepetido. -
Perfilar e medir Use ferramentas como BenchmarkDotNet ou perfis de alocação para medir alocações no hot path e entender o custo real antes de otimizar.
-
Usar estruturas de pooling Estruturas como
ArrayPool<T>,MemoryPool<T>, ou pools customizados podem reduzir a necessidade de alocações repetidas sem comprometer segurança de recursos.
Benchmark de reuso
[MemoryDiagnoser]
public class ReuseOrDispose
{
private MemoryStream _reusedStream;
[GlobalSetup]
public void Setup()
{
_reusedStream = new MemoryStream();
}
[Benchmark]
public void WithUsing()
{
using MemoryStream stream = new();
stream.WriteByte(1);
}
[Benchmark]
public void Reuse()
{
_reusedStream.SetLength(0);
_reusedStream.WriteByte(1);
}
}
Conclusão
Enquanto o using continua sendo uma ferramenta poderosa para liberar recursos de forma determinista, ele não é uma garantia automática de alta performance. Em cenários de alto throughput, o custo de criar e descartar muitos objetos rapidamente pode dominar o tempo de CPU e prejudicar a escalabilidade do sistema. (Medium)
A chave é entender onde e como o using é usado e considerar alternativas — como reutilização ou pooling — quando estiver em caminhos críticos de desempenho.