| 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 Esta Skill
Use quando a tarefa envolver:
- Otimização de performance, redução de alocações
- Span, Memory, ArrayPool, ValueTask
- Hot paths, zero-allocation, queries compiladas
- Palavras-chave: "performance", "otimizar", "alocações", "Span", "ArrayPool", "rápido", "hot path"
⚠️ Importante: Otimizações devem ser guiadas por profiling (medição real). Código legível tem prioridade; otimize apenas quando houver necessidade comprovada.
1. Span e Memory
Span — Manipulação de Arrays sem Alocações
Quando usar: parsing, manipulação de strings, processamento de arrays em hot paths.
public static string ExtractDomain(string email)
{
var atIndex = email.IndexOf('@');
return email.Substring(atIndex + 1);
}
public static ReadOnlySpan<char> ExtractDomain(ReadOnlySpan<char> email)
{
var atIndex = email.IndexOf('@');
return email.Slice(atIndex + 1);
}
var email = "user@example.com".AsSpan();
var domain = ExtractDomain(email);
Parsing com Span
public static int[] ParseNumbers(string input)
{
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;
}
public static void ParseNumbers(ReadOnlySpan<char> input, Span<int> numbers)
{
int index = 0;
int start = 0;
for (int i = 0; i <= input.Length; i++)
{
if (i == input.Length || input[i] == ',')
{
if (i > start)
{
numbers[index++] = int.Parse(input.Slice(start, i - start));
}
start = i + 1;
}
}
}
var input = "10,20,30,40".AsSpan();
Span<int> numbers = stackalloc int[4];
ParseNumbers(input, numbers);
Memory — Span assíncrono-friendly
public async Task<int> ProcessDataAsync(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;
}
2. ArrayPool — Reutilização de Arrays
Quando usar: necessidade temporária de arrays, especialmente em loops ou hot paths.
using System.Buffers;
public byte[] ProcessData(int size)
{
var buffer = new byte[size];
return buffer;
}
public byte[] ProcessDataPooled(int size)
{
var buffer = ArrayPool<byte>.Shared.Rent(size);
try
{
var result = new byte[size];
Array.Copy(buffer, result, size);
return result;
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
public void ProcessDataInPlace(Span<byte> destination)
{
var size = destination.Length;
var buffer = ArrayPool<byte>.Shared.Rent(size);
try
{
var span = buffer.AsSpan(0, size);
span.CopyTo(destination);
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
ArrayPool em Cenários Reais
public class CsvParser
{
public async Task<List<User>> ParseCsvAsync(Stream stream, CancellationToken ct)
{
var users = new List<User>();
var buffer = ArrayPool<byte>.Shared.Rent(8192);
try
{
int bytesRead;
while ((bytesRead = await stream.ReadAsync(buffer, ct)) > 0)
{
var span = buffer.AsSpan(0, bytesRead);
ParseLines(span, users);
}
return users;
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
private void ParseLines(ReadOnlySpan<byte> data, List<User> users)
{
}
}
3. ValueTask — Async sem Alocações
Quando usar: operações que frequentemente completam de forma síncrona (cache, fast path).
public async Task<User?> GetUserAsync(string id)
{
if (_cache.TryGetValue(id, out var user))
return user;
return await LoadUserFromDatabaseAsync(id);
}
public ValueTask<User?> GetUserAsync(string id)
{
if (_cache.TryGetValue(id, out var user))
return ValueTask.FromResult(user);
return new ValueTask<User?>(LoadUserFromDatabaseAsync(id));
}
public async ValueTask<User?> GetUserAsync(string id, CancellationToken ct)
{
if (_cache.TryGetValue(id, out var user))
return user;
user = await LoadUserFromDatabaseAsync(id, ct);
if (user != null)
_cache.Set(id, user);
return user;
}
Quando NÃO Usar ValueTask
public class Example
{
private ValueTask<int> _task;
public async Task<int> AlwaysAsyncMethod()
{
await Task.Delay(100);
return 42;
}
}
4. StringBuilder — Concatenação Eficiente
public string BuildReport(List<string> lines)
{
string result = "";
foreach (var line in lines)
{
result += line + "\n";
}
return result;
}
public string BuildReport(List<string> lines)
{
var sb = new StringBuilder(capacity: lines.Count * 50);
foreach (var line in lines)
{
sb.AppendLine(line);
}
return sb.ToString();
}
public string BuildReport(List<string> lines)
{
return string.Join("\n", lines);
}
5. EF Core — Queries Compiladas
Quando usar: queries executadas frequentemente com mesma estrutura.
public async Task<User?> GetUserByIdAsync(Guid id, CancellationToken ct)
{
return await _context.Users
.AsNoTracking()
.FirstOrDefaultAsync(u => u.Id == id, ct);
}
private static readonly Func<AppDbContext, Guid, Task<User?>> GetUserByIdQuery =
EF.CompileAsyncQuery((AppDbContext context, Guid id) =>
context.Users.AsNoTracking().FirstOrDefault(u => u.Id == id));
public async Task<User?> GetUserByIdAsync(Guid id, CancellationToken ct)
{
return await GetUserByIdQuery(_context, id);
}
private static readonly Func<AppDbContext, string, UserRole, IAsyncEnumerable<User>> GetUsersByEmailAndRoleQuery =
EF.CompileAsyncQuery((AppDbContext context, string emailPattern, UserRole role) =>
context.Users
.AsNoTracking()
.Where(u => u.Email.Contains(emailPattern) && u.Role == role));
public IAsyncEnumerable<User> GetUsersByEmailAndRoleAsync(string emailPattern, UserRole role)
{
return GetUsersByEmailAndRoleQuery(_context, emailPattern, role);
}
6. Caching
IMemoryCache
public class UserService(IMemoryCache cache, IUserRepository repository)
{
public async Task<User?> GetUserAsync(Guid id, CancellationToken ct)
{
var cacheKey = $"user:{id}";
if (cache.TryGetValue(cacheKey, out User? user))
return user;
user = await repository.GetByIdAsync(id, ct);
if (user != null)
{
var cacheOptions = new MemoryCacheEntryOptions()
.SetAbsoluteExpiration(TimeSpan.FromMinutes(10))
.SetSlidingExpiration(TimeSpan.FromMinutes(2));
cache.Set(cacheKey, user, cacheOptions);
}
return user;
}
}
Output Caching (.NET 7+)
builder.Services.AddOutputCache(options =>
{
options.AddBasePolicy(builder => builder
.Expire(TimeSpan.FromMinutes(5))
.Tag("api"));
options.AddPolicy("users", builder => builder
.Expire(TimeSpan.FromMinutes(10))
.Tag("users"));
});
app.UseOutputCache();
[HttpGet("{id}")]
[OutputCache(PolicyName = "users")]
public async Task<IActionResult> GetUserAsync(Guid id)
{
var user = await _useCase.ExecuteAsync(id);
return Ok(user);
}
[HttpPost]
public async Task<IActionResult> CreateUserAsync([FromBody] CreateUserInput input)
{
var result = await _useCase.ExecuteAsync(input);
await _cache.EvictByTagAsync("users", HttpContext.RequestAborted);
return Ok(result);
}
7. Async Streams — IAsyncEnumerable
Quando usar: streaming de grandes volumes de dados.
public async Task<List<Order>> GetOrdersAsync(CancellationToken ct)
{
return await _context.Orders.ToListAsync(ct);
}
public async IAsyncEnumerable<Order> GetOrdersStreamAsync(
[EnumeratorCancellation] CancellationToken ct = default)
{
await foreach (var order in _context.Orders.AsAsyncEnumerable().WithCancellation(ct))
{
yield return order;
}
}
await foreach (var order in orderService.GetOrdersStreamAsync(ct))
{
await ProcessOrderAsync(order, ct);
}
8. Parallel Processing
Parallel.ForEachAsync (.NET 6+)
public async Task ProcessOrdersAsync(List<Order> orders, CancellationToken ct)
{
foreach (var order in orders)
{
await ProcessOrderAsync(order, ct);
}
}
public async Task ProcessOrdersAsync(List<Order> orders, CancellationToken ct)
{
var options = new ParallelOptions
{
MaxDegreeOfParallelism = 10,
CancellationToken = ct
};
await Parallel.ForEachAsync(orders, options, async (order, ct) =>
{
await ProcessOrderAsync(order, ct);
});
}
Task.WhenAll para Batch Processing
public async Task<List<User>> GetUsersAsync(List<Guid> ids, CancellationToken ct)
{
var tasks = ids.Select(id => GetUserAsync(id, ct));
var users = await Task.WhenAll(tasks);
return users.Where(u => u != null).ToList()!;
}
9. Lazy Initialization
public class ExpensiveService
{
private readonly HeavyResource _resource;
public ExpensiveService()
{
_resource = new HeavyResource();
}
}
public class ExpensiveService
{
private readonly Lazy<HeavyResource> _resource = new(() => new HeavyResource());
public void DoWork()
{
_resource.Value.Process();
}
}
public class ExpensiveService
{
private readonly SemaphoreSlim _lock = new(1, 1);
private HeavyResource? _resource;
public async Task<HeavyResource> GetResourceAsync()
{
if (_resource != null)
return _resource;
await _lock.WaitAsync();
try
{
if (_resource == null)
_resource = await HeavyResource.CreateAsync();
return _resource;
}
finally
{
_lock.Release();
}
}
}
10. Profiling e Medição
BenchmarkDotNet
dotnet add package BenchmarkDotNet
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
[MemoryDiagnoser]
public class ParsingBenchmarks
{
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 void ParseWithSpan()
{
Span<int> numbers = stackalloc int[5];
ParseNumbers(Input.AsSpan(), numbers);
}
private void ParseNumbers(ReadOnlySpan<char> input, Span<int> numbers)
{
}
}
public class Program
{
public static void Main(string[] args)
{
BenchmarkRunner.Run<ParsingBenchmarks>();
}
}
Stopwatch para Medições Simples
var sw = Stopwatch.StartNew();
await ProcessDataAsync();
sw.Stop();
logger.LogInformation("Operation took {ElapsedMs}ms", sw.Elapsed.TotalMilliseconds);
11. Boas Práticas
Quando Otimizar
- ✅ Após profiling (não otimize prematuramente)
- ✅ Hot paths identificados (loops internos, operações frequentes)
- ✅ Alocações excessivas detectadas (memory profiler)
- ✅ Latências mensuradas acima do aceitável
Prioridades
- Correto > Rápido
- Legível > Performático (a menos que seja hot path crítico)
- Medido > Assumido (sempre profile antes e depois)
Trade-offs
- Span: performance vs. complexidade (stack-only, não async)
- ValueTask: performance vs. complexidade (não reutilizável)
- ArrayPool: performance vs. gerenciamento manual (lembrar de Return)
- Caching: performance vs. consistência (cache invalidation é difícil)
Checklist de Otimização
Resumo
Esta skill cobre:
- ✅ Span e Memory para zero-allocation
- ✅ ArrayPool para reutilização de buffers
- ✅ ValueTask para async eficiente
- ✅ StringBuilder para concatenação
- ✅ Compiled queries no EF Core
- ✅ Caching (IMemoryCache, Output Cache)
- ✅ IAsyncEnumerable para streaming
- ✅ Parallel processing
- ✅ Profiling e benchmarking
- ✅ Boas práticas e trade-offs
Lembre-se: Otimize apenas quando necessário, sempre guiado por medição real (profiling). Código legível e correto tem prioridade.
Sempre que trabalhar com performance, otimização, alocações ou hot paths, use esta skill como referência.