Introdução: Por que a Performance em C# Importa
No mundo moderno do desenvolvimento de software, a velocidade da aplicação impacta diretamente a experiência do usuário, os custos de infraestrutura e a competitividade do produto. A plataforma .NET e a linguagem C# fornecem ferramentas poderosas para criar sistemas de alto desempenho, no entanto, o uso incorreto de recursos (por exemplo, gerenciamento de memória descontrolado ou algoritmos ineficientes) pode levar a atrasos sérios e consumo excessivo de recursos. Neste artigo, vamos detalhar técnicas-chave para otimizar a performance de aplicações C# que ajudarão você a escrever código mais rápido e eficiente.
1. Profiling: Encontre o Gargalo Antes de Otimizar
Antes de fazer alterações, é necessário determinar com precisão onde o maior tempo é gasto. Sem profiling, você corre o risco de otimizar o que já é rápido, perdendo os verdadeiros gargalos.
Ferramentas de Profiling
- Ferramentas de Diagnóstico do Visual Studio — um profiler integrado para analisar CPU, memória e exceções.
- dotMemory — uma ferramenta poderosa para analisar gerenciamento de memória e encontrar vazamentos.
- PerfView — uma ferramenta gratuita da Microsoft para análise profunda de performance .NET.
- BenchmarkDotNet — uma biblioteca para micro-benchmarking de seções individuais de código.
Exemplo de uso do BenchmarkDotNet para medir a velocidade de dois métodos:
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
public class StringBenchmark
{
private string[] data = Enumerable.Range(0, 1000).Select(i => $"Item {i}").ToArray();
[Benchmark]
public string ConcatenateWithPlus()
{
string result = "";
foreach (var s in data) result += s;
return result;
}
[Benchmark]
public string ConcatenateWithStringBuilder()
{
var sb = new System.Text.StringBuilder();
foreach (var s in data) sb.Append(s);
return sb.ToString();
}
}
// Run: BenchmarkRunner.Run<StringBenchmark>();
Os resultados do benchmark mostrarão claramente que StringBuilder é dezenas de vezes mais rápido que a concatenação usando + em loops.
2. Gerenciamento de Memória e Redução da Pressão no GC
O Garbage Collector (GC) é um mecanismo poderoso, mas sua operação frequente pode impactar negativamente a performance, especialmente em sistemas de alta carga. O objetivo principal é minimizar o número de alocações e o tempo de vida dos objetos.
2.1. Usando Structs em Vez de Classes
Se um objeto é pequeno (menos de 16-24 bytes) e tem um tempo de vida curto, considere usar um struct. Structs são armazenadas na pilha (ou embutidas no objeto pai) e não criam pressão no GC.
// Classe — alocação no heap
public class PointClass
{
public int X { get; set; }
public int Y { get; set; }
}
// Struct — na pilha
public struct PointStruct
{
public int X;
public int Y;
}
2.2. Object Pooling
Para objetos frequentemente criados e destruídos (por exemplo, conexões de banco de dados, buffers), use um pool. O .NET Core tem uma implementação embutida ObjectPool<T>.
using Microsoft.Extensions.ObjectPool;
var pool = new DefaultObjectPool<List<int>>(new ListPolicy());
var list = pool.Get();
try
{
// Trabalhe com a lista
}
finally
{
pool.Return(list); // Retorne ao pool
}
2.3. Span<T> e Memory<T>
Esses tipos permitem trabalhar com regiões contíguas de memória sem alocações adicionais. Eles são ideais para parsing, trabalho com arrays e strings.
public int SumDigits(ReadOnlySpan<