25 个使用.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.
.NET 10 带来了显著的性能提升,但掌握正确的技巧才能发挥最大效果。本文汇集了 25 条实用技巧,帮助您最大限度地提升应用程序性能。
1. Prefira Span para manipulação de dados em memória
1. 在内存中处理数据时优先选择 Span
Span<T> permite trabalhar com fatias de memória sem alocações adicionais. Ideal para parsing e manipulação de strings ou arrays.Span<T> 允许在不进行额外分配的情况下处理内存片段。非常适合解析和字符串或数组的操作。
// 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
2. 使用 FrozenDictionary 和 FrozenSet 处理不可变数据
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.
在.NET 8 中引入并在.NET 10 中优化的"冻结"集合非常适合初始化后不会更改的数据。
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
3. 避免在热点路径中使用闭包
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
4. 根据您的场景配置垃圾回收器
O .NET 10 permite ajuste fino do GC. Para aplicações de alta performance, considere o Server GC com regiões habilitadas.
.NET 10 允许精细调整 GC。对于高性能应用程序,请考虑使用启用区域的 Server GC。
<!-- 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.
对于已知有内存分配峰值的应用, GC.TryStartNoGCRegion() 可以避免在关键时刻发生暂停。
5. Use SearchValues para buscas repetitivas em strings
5. 使用 SearchValues 进行字符串中的重复搜索
SearchValues<T> pré-computa estruturas de busca, tornando operações como IndexOfAny muito mais eficientes.SearchValues<T> 预先计算搜索结构,使 IndexOfAny 等操作更加高效。
// 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
6. 对于经常同步完成的操作,优先选择 ValueTask
Quando um método async frequentemente retorna de forma síncrona (cache hit, por exemplo), ValueTask evita a alocação de Task.
当异步方法频繁同步返回(例如缓存命中时), ValueTask 可以避免 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.ValueTask 不应随意使用。它不能被多次 await,并且在转换为 Task 时可能导致装箱。仅在大多数调用(>90%)同步完成时使用。
7. Utilize ArrayPool e MemoryPool para arrays temporários
7. 使用 ArrayPool 和 MemoryPool 来处理临时数组
Reutilizar arrays através de pools reduz drasticamente as alocações em operações de I/O ou processamento de buffers.
通过使用对象池来重用数组可以大幅减少 I/O 操作或缓冲区处理中的内存分配。
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.
由对象池返回的数组默认情况下不会被清零。如果缓冲区包含敏感数据,请始终使用 clearArray: true 在 Return 中避免信息在多次使用之间泄露。
Para cenários mais complexos, considere MemoryPool<T> que trabalha com Memory<T> e permite maior flexibilidade.
对于更复杂的场景,可以考虑使用 MemoryPool<T> ,它工作于 Memory<T> 并提供更高的灵活性。
8. Implemente ISpanParsable para tipos customizados
8. 为自定义类型实现 ISpanParsable
O .NET 10 favorece parsing baseado em Span. Implemente ISpanParsable<T> para seus tipos de domínio.
.NET 10 优先采用基于 Span 的解析。为您的领域类型实现 ISpanParsable<T> 。
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
9. 使用显式的 StringComparison
Comparações de string sem especificar o tipo usam cultura corrente, que é mais lento e pode causar bugs sutis.
未指定类型的字符串比较使用当前文化,这更慢并且可能导致细微的 bug。
// 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
10. 合理配置连接池
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
11. 对于重复格式化的字符串,优先选择 CompositeFormat
CompositeFormat pré-compila o formato, evitando parsing repetido em logs ou mensagens frequentes.CompositeFormat 预编译格式,避免频繁的日志或消息中的重复解析。
// 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
12. 使用 Source Generators 进行 JSON 序列化
System.Text.Json com source generators elimina reflection em runtime, melhorando performance.System.Text.Json 使用源生成器可以在运行时消除反射,从而提高性能。
[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
13. 避免在不需要的地方使用异步
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
14. 使用 Parallel.ForEachAsync 进行受控的并发 I/O
Para processamento paralelo de I/O, Parallel.ForEachAsync oferece controle fino sobre concorrência.
对于 I/O 的并行处理, Parallel.ForEachAsync 提供了对竞争的精细控制。
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
15. 正确配置 HttpClient
HttpClient deve ser reutilizado. Use IHttpClientFactory para gerenciamento adequado de conexões.HttpClient 应该被重用。使用 IHttpClientFactory 来适当地管理连接。
// 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
16. 使用 stackalloc 处理小缓冲区
Para buffers pequenos e de vida curta, stackalloc evita completamente o uso do heap.
对于小而生命周期短的缓冲区, stackalloc 完全避免使用堆。
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
17. 对于小的数据传输对象,优先选择 Struct Records
Para objetos de transferência pequenos e imutáveis, record struct frequentemente evita alocações no heap.
对于小而不可变的数据传输对象, record struct 经常避免堆分配。
// 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.
避免使用大的结构(超过 16-24 字节)或包含许多字段的结构,因为复制成本可能超过收益。
18. Use Regex Source Generators
18. 使用 Regex 源生成器
Regex compilado via source generator é mais rápido e compatível com Native AOT.
通过源生成器编译的 Regex 更快,并且兼容 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
19. 使用泛型约束最小化装箱
Boxing de value types cria objetos no heap. Use constraints genéricos para evitar.
value types 的装箱会在堆上创建对象。使用泛型约束来避免。
// 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
20. 在 Minimal APIs 中配置输出缓存
O .NET 10 traz melhorias significativas no Output Caching para Minimal APIs.
.NET 10 为 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
21. 使用 Channels 实现生产者-消费者模式
Channel<T> é mais eficiente que BlockingCollection para cenários async de produtor-consumidor.Channel<T> 比 BlockingCollection 在异步生产者-消费者场景中更高效。
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
22. 对于只读查询,优先选择 AsNoTracking
O Entity Framework mantém tracking de entidades por padrão, o que consome memória e CPU.
Entity Framework 默认会跟踪实体,这会消耗内存和 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
23. 使用具有初始容量的 StringBuilder
Para concatenação de strings em loops, StringBuilder com capacidade pré-definida evita realocações.
在循环中进行字符串连接时,具有预定义容量的 StringBuilder 可以避免重新分配。
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
24. 启用动态 PGO
Profile-Guided Optimization dinâmica permite que o JIT otimize baseado no comportamento real da aplicação.
动态 PGO 允许 JIT 根据应用程序的实际行为进行优化。
<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.
动态 PGO 在应用程序预热后可以带来 10-30%的性能提升。
25. Meça ANTES DE OTIMIZAR
25. 在优化之前进行测量
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-counters:GC、线程池和异常的实时指标 - dotnet-trace: Coleta traces para análise detalhada
dotnet-trace: 收集跟踪信息以进行详细分析 - dotnet-dump: Análise de memória e diagnóstico de leaks
dotnet-dump: 内存分析和泄漏诊断 - PerfView: Análise profunda de CPU e alocações
PerfView: 深入分析 CPU 和内存分配 - Visual Studio Profiler: Integração completa com debugging
Visual Studio Profiler: 与调试功能完全集成
# 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.
.NET 10 的性能是应用程序每一层做出的自觉选择的结果。从选择合适的数据结构到正确配置基础设施,每一个决策都会影响最终结果。
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.
记住:过早优化是万恶之源。先测量,识别真正的瓶颈,然后应用适当的技术。这里提出的建议是您工具箱中的工具,或者像我喜欢说的...您的实用工具腰带,在数据表明需要时使用它们。

浙公网安备 33010602011771号