Otimizando contagem de frequência com LINQ CountBy
Substitua GroupBy por CountBy no .NET 9 para uma contagem de frequência mais limpa e eficiente. Reduz alocações de O(N) para O(K) ao pular estruturas intermediárias de agrupamento.
Uma das operações mais comuns em processamento de dados é calcular a frequência de itens em uma coleção. Por anos, desenvolvedores C# contaram com o padrão GroupBy para isso. Embora funcional, ele costuma trazer sobrecarga desnecessária ao alocar objetos de bucket para grupos que são descartados imediatamente após a contagem.
Com o .NET 9, o namespace System.Linq introduz CountBy, um método especializado que simplifica significativamente essa operação.
A sobrecarga legada
Antes do .NET 9, contar ocorrências geralmente exigia uma cadeia verbosa de chamadas LINQ. Você tinha que agrupar os elementos e depois projetá-los em um novo tipo contendo a chave e a contagem.
// Before: Verbose and allocates group buckets
var logLevels = new[] { "INFO", "ERROR", "INFO", "WARN", "ERROR", "INFO" };
var frequency = logLevels
.GroupBy(level => level)
.Select(group => new { Level = group.Key, Count = group.Count() })
.ToDictionary(x => x.Level, x => x.Count);
Essa abordagem funciona, mas é pesada. O iterador GroupBy constrói estruturas internas para guardar os elementos de cada grupo, mesmo que só nos interesse a contagem. Para conjuntos grandes, isso impõe pressão desnecessária sobre o coletor de lixo.
Enxugando com CountBy
O .NET 9 adiciona CountBy diretamente a IEnumerable<T>. Esse método retorna uma coleção de KeyValuePair<TKey, int>, dispensando estruturas intermediárias de agrupamento.
// After: Clean, intent-revealing, and efficient
var logLevels = new[] { "INFO", "ERROR", "INFO", "WARN", "ERROR", "INFO" };
foreach (var (level, count) in logLevels.CountBy(level => level))
{
Console.WriteLine($"{level}: {count}");
}
A sintaxe não é apenas mais limpa; ela declara explicitamente a intenção: estamos contando por uma chave.
Implicações de desempenho
Por baixo dos panos, CountBy é otimizado para evitar alocar os buckets de agrupamento que o GroupBy exige. Em um cenário tradicional de GroupBy, o runtime frequentemente cria um objeto Grouping<TKey, TElement> para cada chave única e mantém internamente uma coleção de elementos para essa chave. Se você tem 1 milhão de itens e 100 chaves únicas, o GroupBy ainda pode fazer trabalho significativo organizando esse 1 milhão de itens em listas.
CountBy, por outro lado, só precisa rastrear o contador. Comporta-se efetivamente como um acumulador Dictionary<TKey, int>. Ele itera a origem uma vez, incrementa o contador para a chave e descarta o elemento. Isso transforma uma operação com espaço O(N) (em termos de manter elementos) em algo mais próximo de O(K) de espaço, onde K é o número de chaves únicas.
Para cenários de alto throughput, como analisar logs de servidor, processar fluxos de transações ou agregar dados de sensores, essa diferença não é trivial. Ela reduz a pressão sobre o GC ao descartar imediatamente os pesados objetos de “bucket”.
Casos extremos e chaves
Como o GroupBy, o CountBy usa o comparador de igualdade padrão do tipo da chave, a menos que outro seja informado. Se você está contando por uma chave de objeto personalizada, garanta que GetHashCode e Equals estão corretamente sobrescritos, ou forneça um IEqualityComparer<TKey> próprio.
// Handling case-insensitivity explicitly
var frequency = logLevels.CountBy(level => level, StringComparer.OrdinalIgnoreCase);
Quando manter o GroupBy
Vale notar que CountBy é estritamente para contagem. Se você precisa dos elementos em si (por exemplo, “me dê os 5 primeiros erros”), ainda precisa do GroupBy. Mas para histogramas, mapas de frequência e analytics, CountBy no .NET 9 é a ferramenta superior.
Ao adotar CountBy, você reduz a verbosidade e melhora os padrões de alocação em seus pipelines LINQ, tornando-o a escolha padrão para análise de frequência em códigos C# modernos.
Comments
Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.