| name | performance-optimization |
| description | Guia para otimização de performance — Span<T>, Memory<T>, ArrayPool, ValueTask, compiled queries e técnicas avançadas. Use quando a tarefa envolver performance, otimização, alocações, memory pooling ou hot paths. |
Performance Optimization — .NET
Quando Usar
- Otimização de performance, redução de alocações
- Span, Memory, ArrayPool, ValueTask
- Hot paths, queries compiladas, zero-allocation
- Palavras-chave: "performance", "otimizar", "alocações", "Span", "ArrayPool", "rápido", "hot path"
⚠️ Importante: Otimize apenas com profiling (medição real). Código legível > performance prematura.
Princípios Essenciais
✅ Fazer
- Usar Span para manipulação de arrays/strings sem alocações (parsing, slicing)
- Usar ArrayPool para arrays temporários (reutilização, menos GC pressure)
- Usar ValueTask quando operação frequentemente completa de forma síncrona
- Compiled queries (EF Core) para queries repetitivas
- AsNoTracking() em EF Core para queries read-only
- Profiling primeiro: medir antes de otimizar (BenchmarkDotNet)
❌ Não Fazer
- Nunca otimizar sem medir (profiling)
- Nunca sacrificar legibilidade por micro-otimizações sem impacto
- Nunca usar
Span<T> em métodos async (usar Memory<T>)
- Nunca esquecer de devolver arrays ao
ArrayPool (usar try/finally)
- Nunca assumir que "mais rápido" = "melhor" (trade-offs)
Regra de ouro: Profile → Otimize → Meça novamente. Span + ArrayPool cobrem 80% dos casos.
Checklist Rápido
- Profile primeiro: BenchmarkDotNet ou dotTrace para identificar hot paths
- Span para parsing, slicing, manipulação de strings sem alocações
- ArrayPool para arrays temporários (Rent → usar → Return no
finally)
- ValueTask para operações que frequentemente completam síncronamente
- AsNoTracking() em queries EF Core read-only
- Compiled queries para queries EF Core repetitivas
- Measure again: validar que otimização teve efeito
Exemplo Mínimo
Cenário: Parsing de CSV com Span e ArrayPool (zero alocações)
Span — Parsing sem Alocações
public static string[] ParseCsvLine(string line)
{
return line.Split(',');
}
public static void ParseCsvLine(ReadOnlySpan<char> line, Span<Range> ranges, out int count)
{
count = 0;
int start = 0;
for (int i = 0; i <= line.Length; i++)
{
if (i == line.Length || line[i] == ',')
{
ranges[count++] = new Range(start, i);
start = i + 1;
}
}
}
var line = "John,Doe,30".AsSpan();
Span<Range> ranges = stackalloc Range[10];
ParseCsvLine(line, ranges, out int count);
for (int i = 0; i < count; i++)
{
var field = line[ranges[i]];
Console.WriteLine(field.ToString());
}
ArrayPool — Reutilização de Arrays
using System.Buffers;
public byte[] ProcessData(int size)
{
var buffer = new byte[size];
return buffer;
}
public void ProcessData(int size, Span<byte> destination)
{
var buffer = ArrayPool<byte>.Shared.Rent(size);
try
{
var span = buffer.AsSpan(0, size);
span.CopyTo(destination);
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
ValueTask — Operações Frequentemente Síncronas
public class CachedUserRepository(IUserRepository repository, IMemoryCache cache)
{
public async ValueTask<User?> GetByIdAsync(Guid id, CancellationToken ct = default)
{
if (cache.TryGetValue(id, out User? cached))
return cached;
var user = await repository.GetByIdAsync(id, ct);
if (user != null)
cache.Set(id, user, TimeSpan.FromMinutes(5));
return user;
}
}
public async Task<User?> GetByIdAsync(Guid id, CancellationToken ct = default)
{
if (cache.TryGetValue(id, out User? cached))
return cached;
}
Pontos-chave:
- Span: parsing, slicing, manipulação sem alocações (só código síncrono)
- ArrayPool: arrays temporários, reduz GC pressure (sempre Return no
finally)
- ValueTask: quando operação frequentemente completa de forma síncrona
Memory — Span para Async
Span<T> não pode ser usado em métodos async (vive no stack). Use Memory<T>:
public async Task<int> ProcessAsync(Memory<byte> buffer, CancellationToken ct)
{
await ReadDataAsync(buffer, ct);
Span<byte> span = buffer.Span;
return ProcessBytes(span);
}
private int ProcessBytes(Span<byte> data)
{
int sum = 0;
foreach (var b in data) sum += b;
return sum;
}
Compiled Queries (EF Core)
Para queries repetitivas, compile uma vez:
private static readonly Func<AppDbContext, Guid, Task<User?>> GetUserByIdQuery =
EF.CompileAsyncQuery((AppDbContext ctx, Guid id) =>
ctx.Users.AsNoTracking().FirstOrDefault(u => u.Id == id));
public async Task<User?> GetByIdAsync(Guid id, CancellationToken ct = default)
{
return await GetUserByIdQuery(context, id);
}
Profiling com BenchmarkDotNet
dotnet add package BenchmarkDotNet
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
[MemoryDiagnoser]
public class ParsingBenchmark
{
private const string Input = "10,20,30,40,50";
[Benchmark(Baseline = true)]
public int[] ParseWithSplit()
{
var parts = Input.Split(',');
var numbers = new int[parts.Length];
for (int i = 0; i < parts.Length; i++)
numbers[i] = int.Parse(parts[i]);
return numbers;
}
[Benchmark]
public int[] ParseWithSpan()
{
var span = Input.AsSpan();
Span<int> numbers = stackalloc int[5];
return numbers.ToArray();
}
}
BenchmarkRunner.Run<ParsingBenchmark>();
Técnicas por Cenário
| Cenário | Técnica | Ganho |
|---|
| Parsing de strings/CSV | Span<T> | Zero alocações |
| Arrays temporários (loops) | ArrayPool<T> | -70% GC pressure |
| Cache/operações síncronas | ValueTask<T> | -50% alocações |
| Queries EF Core repetitivas | Compiled queries | +30% throughput |
| Queries EF Core read-only | AsNoTracking() | +20% performance |
Referências