Introdução: por que o desempenho é importante desde o início?
\nTodo desenvolvedor C# mais cedo ou mais tarde se depara com uma situação em que a aplicação começa a ficar lenta. Frequentemente, os problemas de desempenho são introduzidos nas fases iniciais — devido a algoritmos não otimizados, gerenciamento incorreto de memória ou uso excessivo de LINQ sem entender seu funcionamento interno. Para iniciantes, este tópico pode parecer complexo, mas, na verdade, os princípios básicos de otimização são bastante simples e podem ser aprendidos em algumas horas de prática.
\nNeste artigo, abordaremos quatro aspectos-chave que o ajudarão a escrever aplicações C# rápidas e eficientes na plataforma .NET. Você aprenderá como criar perfis de código, otimizar consultas LINQ, gerenciar a memória corretamente e usar assincronia. Os exemplos de código serão o mais próximos possível de tarefas reais que os desenvolvedores enfrentam.
\n\n1. Criação de perfil: o primeiro passo para a otimização
\nAntes de otimizar qualquer coisa, é preciso entender o que está causando a lentidão. Palpites intuitivos são frequentemente errados. A criação de perfil é o processo de medir o desempenho do seu código usando ferramentas especiais.
\n\nFerramentas para criação de perfil
\n- \n
- Ferramentas de Diagnóstico do Visual Studio — ferramenta integrada, disponível nas edições Community e superiores. Permite monitorar o uso de CPU, memória e tempo de execução. \n
- JetBrains dotMemory / dotTrace — poderosas utilidades comerciais para análise de memória e tempo de execução. \n
- BenchmarkDotNet — biblioteca leve para microbenchmarking. Ideal para comparar duas implementações de um mesmo método. \n
Exemplo: medindo o tempo de execução
\nUsamos Stopwatch para uma medição simples:
using System.Diagnostics;\n\nvar stopwatch = Stopwatch.StartNew();\n// Seu código\nfor (int i = 0; i < 1000000; i++)\n{\n // Alguma operação\n}\nstopwatch.Stop();\nConsole.WriteLine($"Tempo de execução: {stopwatch.ElapsedMilliseconds} ms");\nDica: para medições precisas, use BenchmarkDotNet, pois o Stopwatch não considera a sobrecarga da compilação JIT.
2. Gerenciamento de memória e strings
\nO gerenciamento de memória no .NET é automatizado (Garbage Collector), mas isso não significa que você pode ignorá-lo. Um dos problemas mais comuns é a criação excessiva de strings.
\n\nProblema: concatenação de strings em loop
\nStrings em C# são imutáveis. Cada operação + cria uma nova string, o que leva a alocações constantes e pressão sobre o GC:
string result = "";\nfor (int i = 0; i < 10000; i++)\n{\n result += i.ToString(); // RUIM: cria 10000 novas strings\n}\n\nSolução: StringBuilder
\nA classe StringBuilder trabalha com um buffer de caracteres e minimiza as alocações:
var sb = new StringBuilder();\nfor (int i = 0; i < 10000; i++)\n{\n sb.Append(i);\n}\nstring result = sb.ToString(); // BOM: uma alocação no final\n\nEvite boxing
\nBoxing (empacotamento de tipos de valor em um objeto) é outra fonte de alocações ocultas. Exemplo:
\nArrayList list = new ArrayList();\nlist.Add(42); // Boxing: int é empacotado em object\n\n// Melhor usar coleções genéricas:\nList<int> list2 = new List<int>();\nlist2.Add(42); // Sem boxing\n\n3. Otimização de consultas LINQ
\nLINQ é uma ferramenta poderosa, mas seu uso incorreto pode tornar a aplicação muito mais lenta. O principal problema são as múltiplas iterações sobre coleções.
\n\nProblema: iterações múltiplas
\nCada chamada a Where(), Select(), Count() pode iterar sobre a coleção novamente, se o resultado não for materializado:
var numbers = Enumerable.Range(1, 1000000);\nvar evenNumbers = numbers.Where(n => n % 2 == 0);\nint coun