VOOZH about

URL: https://dev.to/unhacked/25-dicas-de-performance-com-net-10-368p

⇱ 25 Dicas de performance com .NET 10 - DEV Community


O .NET 10 traz melhorias significativas de performance, mas conhecer as técnicas certas faz toda a diferença. Este artigo reúne 25 dicas práticas para extrair o máximo de suas aplicações.

1. Prefira Span para manipulação de dados em memória

Span<T> permite trabalhar com fatias de memória sem alocações adicionais. Ideal para parsing e manipulação de strings ou arrays.

// Evite: cria substring (alocação)
string texto = "nome:valor";
string valor = texto.Substring(5);

// Prefira: zero alocações
ReadOnlySpan<char> span = texto.AsSpan();
ReadOnlySpan<char> valorSpan = span.Slice(5);

O ganho é especialmente relevante em loops ou operações de alto throughput, onde cada alocação evitada reduz a pressão sobre o Garbage Collector.

2. Use FrozenDictionary e FrozenSet para dados imutáveis

Introduzidas no .NET 8 e otimizadas no .NET 10, as coleções "frozen" são ideais para dados que não mudam após a inicialização.

using System.Collections.Frozen;

// Dados de configuração que nunca mudam
var statusCodes = new Dictionary<int, string>
{
 [200] = "OK",
 [404] = "Not Found",
 [500] = "Internal Server Error"
}.ToFrozenDictionary();

// Lookup significativamente mais rápido que Dictionary regular
var descricao = statusCodes[200];

O custo de criação é maior, mas as leituras subsequentes são significativamente mais rápidas devido às otimizações internas de hash (hashing otimizado para lookup). Os ganhos variam conforme o dataset, então sempre meça no seu cenário.

3. Evite Closures em Hot Paths

Closures podem gerar alocações, especialmente quando capturam variáveis externas. Em código executado frequentemente, isso impacta a performance.

// Evite: closure captura 'multiplicador'
int multiplicador = 10;
var resultado = lista.Select(x => x * multiplicador).ToList();

// Prefira: use um loop simples para evitar a captura
var resultado = new List<int>(lista.Count);
foreach (var item in lista)
{
 resultado.Add(item * multiplicador);
}

4. Configure o Garbage Collector para seu cenário

O .NET 10 permite ajuste fino do GC. Para aplicações de alta performance, considere o Server GC com regiões habilitadas.

<!-- No .csproj ou runtimeconfig.json -->
<PropertyGroup>
 <ServerGarbageCollection>true</ServerGarbageCollection>
 <GarbageCollectionAdaptationMode>1</GarbageCollectionAdaptationMode>
</PropertyGroup>
// Ou via código para cenários específicos
GCSettings.LatencyMode = GCLatencyMode.SustainedLowLatency;

Para aplicações com picos de alocação conhecidos, GC.TryStartNoGCRegion() pode evitar pausas em momentos críticos.

5. Use SearchValues para buscas repetitivas em strings

SearchValues<T> pré-computa estruturas de busca, tornando operações como IndexOfAny muito mais eficientes.

// Pré-compute uma vez
private static readonly SearchValues<char> Separadores = 
 SearchValues.Create([' ', ',', ';', '\t', '\n']);

// Use múltiplas vezes
public int ContarPalavras(ReadOnlySpan<char> texto)
{
 int count = 1;
 int index;
 while ((index = texto.IndexOfAny(Separadores)) >= 0)
 {
 count++;
 texto = texto.Slice(index + 1);
 }
 return count;
}

6. Prefira ValueTask para operações que frequentemente completam sincronamente

Quando um método async frequentemente retorna de forma síncrona (cache hit, por exemplo), ValueTask evita a alocação de Task.

private readonly ConcurrentDictionary<string, Produto> _cache = new();

// ValueTask evita alocação quando há cache hit
public ValueTask<Produto?> ObterProdutoAsync(string id)
{
 if (_cache.TryGetValue(id, out var produto))
 {
 return ValueTask.FromResult<Produto?>(produto);
 }

 return ObterProdutoDoBancoAsync(id);
}

private async ValueTask<Produto?> ObterProdutoDoBancoAsync(string id)
{
 var produto = await _repository.GetByIdAsync(id);
 if (produto is not null)
 {
 _cache.TryAdd(id, produto);
 }
 return produto;
}

ValueTask não deve ser usado indiscriminadamente. Ele não pode ser awaited múltiplas vezes e pode causar boxing se convertido para Task. Use apenas quando a maioria das chamadas (>90%) completa de forma síncrona.

7. Utilize ArrayPool e MemoryPool para arrays temporários

Reutilizar arrays através de pools reduz drasticamente as alocações em operações de I/O ou processamento de buffers.

public async Task ProcessarArquivoAsync(Stream stream)
{
 byte[] buffer = ArrayPool<byte>.Shared.Rent(8192);
 try
 {
 int bytesRead;
 while ((bytesRead = await stream.ReadAsync(buffer)) > 0)
 {
 ProcessarBloco(buffer.AsSpan(0, bytesRead));
 }
 }
 finally
 {
 // clearArray: true evita vazamento de dados entre usos
 ArrayPool<byte>.Shared.Return(buffer, clearArray: true);
 }
}

Os Arrays retornados pelo pool não são zerados por padrão. Se o buffer contiver dados sensíveis, sempre use clearArray: true no Return para evitar vazamento de informações entre usos.

Para cenários mais complexos, considere MemoryPool<T> que trabalha com Memory<T> e permite maior flexibilidade.

8. Implemente ISpanParsable para tipos customizados

O .NET 10 favorece parsing baseado em Span. Implemente ISpanParsable<T> para seus tipos de domínio.

public readonly record struct Cpf : ISpanParsable<Cpf>
{
 private readonly long _valor;

 public static Cpf Parse(ReadOnlySpan<char> s, IFormatProvider? provider)
 {
 // Remove pontuação sem alocar
 Span<char> digitos = stackalloc char[11];
 int pos = 0;

 foreach (char c in s)
 {
 if (char.IsDigit(c) && pos < 11)
 digitos[pos++] = c;
 }

 if (pos != 11)
 throw new FormatException("CPF deve ter 11 dígitos");

 return new Cpf(long.Parse(digitos));
 }

 public static bool TryParse(ReadOnlySpan<char> s, IFormatProvider? provider, out Cpf result)
 {
 // Implementação similar com tratamento de erro
 }
}

9. Use StringComparison explícito

Comparações de string sem especificar o tipo usam cultura corrente, que é mais lento e pode causar bugs sutis.

// Evite: usa CurrentCulture implicitamente
bool igual = str1.Equals(str2);
bool contem = texto.Contains("busca");

// Prefira: significativamente mais rápido para comparações ordinárias
bool igual = str1.Equals(str2, StringComparison.Ordinal);
bool contem = texto.Contains("busca", StringComparison.OrdinalIgnoreCase);

// Para dicionários
var dict = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);

10. Configure Connection Pooling adequadamente

Conexões de banco de dados são recursos caros. Configure o pool de acordo com sua carga.

// Para SQL Server / PostgreSQL
var connectionString = new NpgsqlConnectionStringBuilder
{
 Host = "localhost",
 Database = "mydb",
 Username = "user",
 Password = "pass",

 // Pool settings
 MinPoolSize = 10,
 MaxPoolSize = 100,
 ConnectionIdleLifetime = 300,
 ConnectionPruningInterval = 10
}.ToString();

// Para Entity Framework Core
services.AddDbContextPool<AppDbContext>(options =>
{
 options.UseNpgsql(connectionString);
}, poolSize: 128);

11. Prefira CompositeFormat para Strings formatadas repetidamente

CompositeFormat pré-compila o formato, evitando parsing repetido em logs ou mensagens frequentes.

// Pré-compile uma vez
private static readonly CompositeFormat LogFormat = 
 CompositeFormat.Parse("[{0:HH:mm:ss}] {1}: {2}");

// Use múltiplas vezes sem parsing repetido
public void Log(string nivel, string mensagem)
{
 var linha = string.Format(null, LogFormat, DateTime.Now, nivel, mensagem);
 Console.WriteLine(linha);
}

12. Use Source Generators para serialização JSON

System.Text.Json com source generators elimina reflection em runtime, melhorando performance.

[JsonSerializable(typeof(Produto))]
[JsonSerializable(typeof(List<Produto>))]
[JsonSourceGenerationOptions(
 PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
 DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
public partial class AppJsonContext : JsonSerializerContext { }

// Uso
var json = JsonSerializer.Serialize(produto, AppJsonContext.Default.Produto);
var produto = JsonSerializer.Deserialize(json, AppJsonContext.Default.Produto);

13. Evite Async em Métodos que não precisam

O overhead de async/await inclui a criação da máquina de estados. Se o resultado já está disponível, retorne diretamente.

// Evite: async desnecessário
public async Task<int> ObterValorAsync()
{
 return await Task.FromResult(42);
}

// Prefira: retorno direto
public Task<int> ObterValorAsync()
{
 return Task.FromResult(42);
}

// Para validações antes de operações async
public Task<Resultado> ProcessarAsync(Dados dados)
{
 if (dados is null)
 return Task.FromResult(Resultado.Invalido);

 return ProcessarInternoAsync(dados);
}

14. Utilize Parallel.ForEachAsync para I/O concorrente controlado

Para processamento paralelo de I/O, Parallel.ForEachAsync oferece controle fino sobre concorrência.

public async Task ProcessarUrlsAsync(IEnumerable<string> urls)
{
 var options = new ParallelOptions
 {
 MaxDegreeOfParallelism = 10 // Limite de requisições simultâneas
 };

 await Parallel.ForEachAsync(urls, options, async (url, ct) =>
 {
 var conteudo = await _httpClient.GetStringAsync(url, ct);
 await ProcessarConteudoAsync(conteudo);
 });
}

15. Configure HttpClient corretamente

HttpClient deve ser reutilizado. Use IHttpClientFactory para gerenciamento adequado de conexões.

// Configuração
services.AddHttpClient("api", client =>
{
 client.BaseAddress = new Uri("https://api.exemplo.com");
 client.DefaultRequestHeaders.Add("Accept", "application/json");
})
.ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
{
 PooledConnectionLifetime = TimeSpan.FromMinutes(2),
 PooledConnectionIdleTimeout = TimeSpan.FromMinutes(1),
 MaxConnectionsPerServer = 100,
 EnableMultipleHttp2Connections = true
});

// Uso
public class MeuServico(IHttpClientFactory factory)
{
 private readonly HttpClient _client = factory.CreateClient("api");
}

16. Use stackalloc para Buffers Pequenos

Para buffers pequenos e de vida curta, stackalloc evita completamente o uso do heap.

public string FormatarCpf(long cpf)
{
 Span<char> buffer = stackalloc char[14]; // XXX.XXX.XXX-XX

 cpf.TryFormat(buffer.Slice(0, 3), out _);
 buffer[3] = '.';
 // ... resto da formatação

 return new string(buffer);
}

// Com limite de segurança para tamanhos variáveis
public void ProcessarDados(int tamanho)
{
 Span<byte> buffer = tamanho <= 256
 ? stackalloc byte[tamanho]
 : new byte[tamanho];

 // Processar...
}

17. Prefira Struct Records para DTOs pequenos

Para objetos de transferência pequenos e imutáveis, record struct frequentemente evita alocações no heap.

// Alocado na stack, sem pressão no GC
public readonly record struct Coordenada(double Latitude, double Longitude);

public readonly record struct ResultadoPaginado<T>(
 T[] Items,
 int Total,
 int Pagina,
 int TamanhoPagina
);

Evite structs grandes (mais de 16-24 bytes) ou com muitos campos, pois o custo de cópia pode superar o benefício.

18. Use Regex Source Generators

Regex compilado via source generator é mais rápido e compatível com Native AOT.

public partial class Validadores
{
 [GeneratedRegex(@"^[\w\.-]+@[\w\.-]+\.\w{2,}$", RegexOptions.IgnoreCase)]
 private static partial Regex EmailRegex();

 [GeneratedRegex(@"^\d{5}-?\d{3}$")]
 private static partial Regex CepRegex();

 public static bool ValidarEmail(string email) => EmailRegex().IsMatch(email);
 public static bool ValidarCep(string cep) => CepRegex().IsMatch(cep);
}

19. Minimize Boxing com Generics Constraints

Boxing de value types cria objetos no heap. Use constraints genéricos para evitar.

// Evite: boxing implícito
public void Processar(IComparable valor) { }

// Prefira: sem boxing
public void Processar<T>(T valor) where T : IComparable<T> { }

// Para interfaces numéricas (.NET 7+)
public T Somar<T>(T a, T b) where T : INumber<T>
{
 return a + b;
}

20. Configure Output Caching em Minimal APIs

O .NET 10 traz melhorias significativas no Output Caching para Minimal APIs.

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOutputCache(options =>
{
 options.AddBasePolicy(builder => builder.Expire(TimeSpan.FromMinutes(5)));

 options.AddPolicy("PorUsuario", builder => builder
 .SetVaryByHeader("Authorization")
 .Expire(TimeSpan.FromMinutes(1)));
});

var app = builder.Build();
app.UseOutputCache();

app.MapGet("/produtos", async (AppDbContext db) =>
 await db.Produtos.ToListAsync())
 .CacheOutput(x => x.Expire(TimeSpan.FromMinutes(10)));

app.MapGet("/perfil", async (HttpContext ctx, AppDbContext db) =>
 await db.Usuarios.FindAsync(ctx.User.GetId()))
 .CacheOutput("PorUsuario");

21. Use Channels para Produtor-Consumidor

Channel<T> é mais eficiente que BlockingCollection para cenários async de produtor-consumidor.

public class FilaDeProcessamento
{
 private readonly Channel<Trabalho> _channel = Channel.CreateBounded<Trabalho>(
 new BoundedChannelOptions(1000)
 {
 FullMode = BoundedChannelFullMode.Wait,
 SingleReader = true,
 SingleWriter = false
 });

 public async ValueTask EnfileirarAsync(Trabalho trabalho, CancellationToken ct = default)
 {
 await _channel.Writer.WriteAsync(trabalho, ct);
 }

 public async Task ProcessarAsync(CancellationToken ct)
 {
 await foreach (var trabalho in _channel.Reader.ReadAllAsync(ct))
 {
 await ProcessarTrabalhoAsync(trabalho);
 }
 }
}

22. Prefira AsNoTracking para Queries somente leitura

O Entity Framework mantém tracking de entidades por padrão, o que consome memória e CPU.

// Para queries de leitura
var produtos = await _context.Produtos
 .AsNoTracking()
 .Where(p => p.Ativo)
 .ToListAsync();

// Configure globalmente para contextos de leitura
services.AddDbContext<ReadOnlyDbContext>(options =>
{
 options.UseNpgsql(connectionString)
 .UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
});

23. Utilize StringBuilder com capacidade inicial

Para concatenação de strings em loops, StringBuilder com capacidade pré-definida evita realocações.

public string GerarRelatorio(IReadOnlyList<Venda> vendas)
{
 // Estime a capacidade: ~50 chars por linha
 var sb = new StringBuilder(vendas.Count * 50);

 foreach (var venda in vendas)
 {
 sb.Append(venda.Data.ToString("dd/MM/yyyy"));
 sb.Append(" - ");
 sb.Append(venda.Cliente);
 sb.Append(": R$ ");
 sb.AppendLine(venda.Valor.ToString("N2"));
 }

 return sb.ToString();
}

24. Habilite Dynamic PGO

Profile-Guided Optimization dinâmica permite que o JIT otimize baseado no comportamento real da aplicação.

<PropertyGroup>
 <TieredPGO>true</TieredPGO>
</PropertyGroup>

Ou via variável de ambiente:

DOTNET_TieredPGO=1
DOTNET_TC_QuickJitForLoops=1
DOTNET_ReadyToRun=0

O Dynamic PGO pode trazer ganhos de 10-30% em aplicações de longa execução após o warm-up.

25. Meça ANTES DE OTIMIZAR

A dica mais importante: use ferramentas de profiling para identificar gargalos reais.

// BenchmarkDotNet para microbenchmarks
[MemoryDiagnoser]
[SimpleJob(RuntimeMoniker.Net100)]
public class MeusBenchmarks
{
 [Benchmark(Baseline = true)]
 public void AbordagemOriginal() { }

 [Benchmark]
 public void AbordagemOtimizada() { }
}

Ferramentas essenciais para profiling:

  • dotnet-counters: Métricas em tempo real de GC, threadpool, exceções
  • dotnet-trace: Coleta traces para análise detalhada
  • dotnet-dump: Análise de memória e diagnóstico de leaks
  • PerfView: Análise profunda de CPU e alocações
  • Visual Studio Profiler: Integração completa com debugging
# Monitorar métricas básicas
dotnet-counters monitor --process-id <PID> --counters System.Runtime

# Coletar trace de 30 segundos
dotnet-trace collect --process-id <PID> --duration 00:00:30

Conclusão

Performance em .NET 10 é resultado de escolhas conscientes em cada camada da aplicação. Desde a escolha de estruturas de dados apropriadas até a configuração adequada de infraestrutura, cada decisão impacta o resultado final.

Lembre-se: otimização prematura é a raiz de todo mal. Meça primeiro, identifique os gargalos reais, e então aplique as técnicas apropriadas. As dicas apresentadas aqui são ferramentas no seu arsenal, ou como eu costumo chamar... seu cinto de utilidades, use-as quando os dados indicarem necessidade.