Performance Optimization — .NET
Quando Usar
- Otimização de performance, redução de alocações
- Span<T>, Memory<T>, ArrayPool<T>, ValueTask<T>
- 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<T> para manipulação de arrays/strings sem alocações (parsing, slicing)
- Usar ArrayPool<T> para arrays temporários (reutilização, menos GC pressure)
- Usar ValueTask<T> 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 (usarMemory<T>) - Nunca esquecer de devolver arrays ao
ArrayPool(usartry/finally) - Nunca assumir que "mais rápido" = "melhor" (trade-offs)
Regra de ouro: Profile → Otimize → Meça novamente. Span<T> + ArrayPool<T> cobrem 80% dos casos.
Checklist Rápido
- Profile primeiro: BenchmarkDotNet ou dotTrace para identificar hot paths
- Span<T> para parsing, slicing, manipulação de strings sem alocações
- ArrayPool<T> para arrays temporários (Rent → usar → Return no
finally) - ValueTask<T> 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<T> e ArrayPool<T> (zero alocações)
Span<T> — Parsing sem Alocações
csharp1// ❌ Alocações desnecessárias 2public static string[] ParseCsvLine(string line) 3{ 4 return line.Split(','); // Aloca array de strings 5} 6 7// ✅ Zero alocações com Span 8public static void ParseCsvLine(ReadOnlySpan<char> line, Span<Range> ranges, out int count) 9{ 10 count = 0; 11 int start = 0; 12 13 for (int i = 0; i <= line.Length; i++) 14 { 15 if (i == line.Length || line[i] == ',') 16 { 17 ranges[count++] = new Range(start, i); 18 start = i + 1; 19 } 20 } 21} 22 23// Uso 24var line = "John,Doe,30".AsSpan(); 25Span<Range> ranges = stackalloc Range[10]; // No stack, zero alocação 26ParseCsvLine(line, ranges, out int count); 27 28for (int i = 0; i < count; i++) 29{ 30 var field = line[ranges[i]]; // ReadOnlySpan<char>, zero alocação 31 Console.WriteLine(field.ToString()); 32}
ArrayPool<T> — Reutilização de Arrays
csharp1using System.Buffers; 2 3// ❌ Alocação a cada chamada 4public byte[] ProcessData(int size) 5{ 6 var buffer = new byte[size]; // GC pressure 7 // ... processa 8 return buffer; 9} 10 11// ✅ Reutilização com ArrayPool 12public void ProcessData(int size, Span<byte> destination) 13{ 14 var buffer = ArrayPool<byte>.Shared.Rent(size); // Reutiliza array 15 16 try 17 { 18 var span = buffer.AsSpan(0, size); 19 // ... processa span 20 span.CopyTo(destination); 21 } 22 finally 23 { 24 ArrayPool<byte>.Shared.Return(buffer); // Devolve ao pool 25 } 26}
ValueTask<T> — Operações Frequentemente Síncronas
csharp1// ✅ ValueTask quando operação pode ser síncrona (ex.: cache hit) 2public class CachedUserRepository(IUserRepository repository, IMemoryCache cache) 3{ 4 public async ValueTask<User?> GetByIdAsync(Guid id, CancellationToken ct = default) 5 { 6 // Cache hit: retorna de forma síncrona (sem alocação de Task) 7 if (cache.TryGetValue(id, out User? cached)) 8 return cached; 9 10 // Cache miss: chama repositório (assíncrono) 11 var user = await repository.GetByIdAsync(id, ct); 12 if (user != null) 13 cache.Set(id, user, TimeSpan.FromMinutes(5)); 14 15 return user; 16 } 17} 18 19// ❌ Task<T> sempre aloca mesmo quando síncrono 20public async Task<User?> GetByIdAsync(Guid id, CancellationToken ct = default) 21{ 22 if (cache.TryGetValue(id, out User? cached)) 23 return cached; // Ainda aloca Task<User> 24 // ... 25}
Pontos-chave:
- Span<T>: parsing, slicing, manipulação sem alocações (só código síncrono)
- ArrayPool<T>: arrays temporários, reduz GC pressure (sempre Return no
finally) - ValueTask<T>: quando operação frequentemente completa de forma síncrona
Memory<T> — Span para Async
Span<T> não pode ser usado em métodos async (vive no stack). Use Memory<T>:
csharp1public async Task<int> ProcessAsync(Memory<byte> buffer, CancellationToken ct) 2{ 3 await ReadDataAsync(buffer, ct); // Memory pode ser passado para async 4 5 Span<byte> span = buffer.Span; // Converter para Span quando necessário 6 return ProcessBytes(span); 7} 8 9private int ProcessBytes(Span<byte> data) 10{ 11 int sum = 0; 12 foreach (var b in data) sum += b; 13 return sum; 14}
Compiled Queries (EF Core)
Para queries repetitivas, compile uma vez:
csharp1private static readonly Func<AppDbContext, Guid, Task<User?>> GetUserByIdQuery = 2 EF.CompileAsyncQuery((AppDbContext ctx, Guid id) => 3 ctx.Users.AsNoTracking().FirstOrDefault(u => u.Id == id)); 4 5public async Task<User?> GetByIdAsync(Guid id, CancellationToken ct = default) 6{ 7 return await GetUserByIdQuery(context, id); 8}
Profiling com BenchmarkDotNet
bash1dotnet add package BenchmarkDotNet
csharp1using BenchmarkDotNet.Attributes; 2using BenchmarkDotNet.Running; 3 4[MemoryDiagnoser] 5public class ParsingBenchmark 6{ 7 private const string Input = "10,20,30,40,50"; 8 9 [Benchmark(Baseline = true)] 10 public int[] ParseWithSplit() 11 { 12 var parts = Input.Split(','); 13 var numbers = new int[parts.Length]; 14 for (int i = 0; i < parts.Length; i++) 15 numbers[i] = int.Parse(parts[i]); 16 return numbers; 17 } 18 19 [Benchmark] 20 public int[] ParseWithSpan() 21 { 22 var span = Input.AsSpan(); 23 Span<int> numbers = stackalloc int[5]; 24 // ... parsing com span 25 return numbers.ToArray(); 26 } 27} 28 29// Program.cs 30BenchmarkRunner.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 |