Forem

Cristiano Rodrigues for Unhacked

Posted on

25 Dicas de performance com .NET 10

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);
Enter fullscreen mode Exit fullscreen mode

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];
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode
// Ou via código para cenários específicos
GCSettings.LatencyMode = GCLatencyMode.SustainedLowLatency;
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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);
    });
}
Enter fullscreen mode Exit fullscreen mode

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");
}
Enter fullscreen mode Exit fullscreen mode

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...
}
Enter fullscreen mode Exit fullscreen mode

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
);
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Ou via variável de ambiente:

DOTNET_TieredPGO=1
DOTNET_TC_QuickJitForLoops=1
DOTNET_ReadyToRun=0
Enter fullscreen mode Exit fullscreen mode

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() { }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)