Introdução: por que o desempenho do C# é importante
C# e a plataforma .NET são ferramentas poderosas para criar serviços web de alta carga, aplicativos desktop e jogos em Unity. No entanto, mesmo o código mais elegante pode ser lento se não levar em conta as particularidades do CLR (Common Language Runtime), da coleta de lixo (GC) e dos mecanismos de compilação JIT.
Neste artigo, analisaremos os principais métodos de otimização de desempenho de aplicações C# que o ajudarão a reduzir o tempo de resposta, diminuir o consumo de memória e aumentar a capacidade de processamento do sistema. Todas as dicas são acompanhadas de exemplos práticos de código.
1. Gerenciamento de memória e coleta de lixo (GC)
Uma das principais características do .NET é o gerenciamento automático de memória. Mas o uso incorreto de objetos leva a coletas de lixo frequentes, causando "pausas" (pause) na aplicação.
1.1. Evite alocações em caminhos críticos (hot paths)
Tente não criar novos objetos dentro de loops ou métodos chamados com frequência. Em vez disso, use pools de objetos ou reutilize instâncias.
// Ruim: um novo StringBuilder é criado a cada iteraçãofor (int i = 0; i < 1000; i++){ var sb = new StringBuilder(); sb.Append(i);}
// Bom: usamos um pool ou reutilizamosvar sbPool = new StringBuilder();for (int i = 0; i < 1000; i++){ sbPool.Clear(); sbPool.Append(i);}1.2. Use structs em vez de classes para dados pequenos
Structs são armazenadas na pilha (stack) e não criam carga para o GC. Mas tenha cuidado: structs grandes (mais de 16 bytes) podem ser copiadas lentamente.
public struct Point{ public int X; public int Y;}
// Agora a chamada do método não cria um objeto no heapvoid ProcessPoint(Point p) { /* ... */ }1.3. Ative o modo Server GC para aplicações servidoras
Por padrão, em aplicações de console, é usado o Workstation GC. Para serviços web (ASP.NET Core), mude para o Server GC no arquivo csproj:
<PropertyGroup> <ServerGarbageCollection>true</ServerGarbageCollection></PropertyGroup>2. Otimização do trabalho com LINQ e coleções
LINQ é uma ferramenta conveniente, mas potencialmente lenta, se você não conhecer suas "armadilhas".
2.1. Evite enumerações múltiplas
Cada chamada a .Count(), .ToList() ou .Any() pode reexecutar a consulta. Salve o resultado em uma variável.
// Ruim: duas passagens pela coleçãoif (items.Where(x => x.IsActive).Count() > 0) { /* ... */ }var activeList = items.Where(x => x.IsActive).ToList();
// Bom: uma passagemvar activeItems = items.Where(x => x.IsActive).ToList();if (activeItems.Count > 0) { /* ... */ }2.2. Use coleções especializadas
Para buscas frequentes por chave, use Dictionary<TKey, TValue> em vez de List<T>. Para operações "primeiro a entrar, primeiro a sair" (FIFO), use Queue<T>.
// Busca rápida: O(1) em vez de O(n)var dict = new Dictionary<int, string>();dict.Add(1, "one");if (dict.ContainsKey(1)) { /* ... */ }2.3. Substitua LINQ por loops em seções críticas
Para operações simples (soma, filtragem), um loop for ou foreach pode ser 2 a 3 vezes mais rápido que o LINQ.
// LINQ (lento)int sum = numbers.Sum();
// Loop (rápido)int sum = 0;for (int i = 0; i < numbers.Length; i++){ sum += numbers[i];}3. Programação assíncrona e redução de bloqueios
O uso correto de async/await permite não bloquear threads, o que é crítico para a escalabilidade.