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 (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<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
csharp
1// ❌ 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
csharp
1using 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
csharp
1// ✅ 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>:
csharp
1public 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:
csharp
1private 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}
bash
1dotnet add package BenchmarkDotNet
csharp
1using 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 |
Referências