Performance e Otimização em .NET para Desenvolvedores Senior
📋 Tópicos Essenciais
1. Memory Management
Garbage Collection
O que é: O Garbage Collector (GC) do .NET gerencia automaticamente a memória, liberando objetos não utilizados.
Como funciona:
- Geração 0: Objetos recém-criados, coleta mais frequente
- Geração 1: Objetos que sobreviveram à primeira coleta
- Geração 2: Objetos de longa duração, coleta menos frequente
Por que é importante: Evita vazamentos de memória e otimiza o uso de recursos do sistema.
Quando usar: Sempre que trabalhar com objetos grandes ou muitos objetos pequenos.
public class MemoryOptimizedService
{
private readonly WeakReference<LargeObject> _cachedObject;
public MemoryOptimizedService()
{
_cachedObject = new WeakReference<LargeObject>(new LargeObject());
}
public LargeObject GetCachedObject()
{
if (_cachedObject.TryGetTarget(out var obj))
return obj;
var newObj = new LargeObject();
_cachedObject.SetTarget(newObj);
return newObj;
}
}IDisposable Pattern
O que é: Padrão para liberar recursos não gerenciados explicitamente.
Como funciona:
- Implementa
IDisposablepara liberar recursos - Usa
usingstatement para liberação automática - Chama
GC.SuppressFinalizepara evitar finalização desnecessária
Por que é importante: Evita vazamentos de recursos como arquivos, conexões de banco, handles do sistema.
Quando usar: Sempre que trabalhar com recursos não gerenciados (Streams, DbContext, Timer, etc.).
public class ResourceManager : IDisposable
{
private bool _disposed = false;
private readonly Stream _stream;
private readonly Timer _timer;
public ResourceManager()
{
_stream = new MemoryStream();
_timer = new Timer(Callback, null, TimeSpan.Zero, TimeSpan.FromSeconds(1));
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed && disposing)
{
_stream?.Dispose();
_timer?.Dispose();
_disposed = true;
}
}
~ResourceManager()
{
Dispose(false);
}
}2. Async/Await Performance
ConfigureAwait Usage
O que é: Configuração que controla se a continuação deve executar no contexto original.
Como funciona:
ConfigureAwait(false): Continua em qualquer thread disponívelConfigureAwait(true): Continua no contexto original (padrão)- Evita deadlocks em aplicações desktop/WPF
Por que é importante: Melhora performance evitando marshalling desnecessário entre threads.
Quando usar: Em bibliotecas e serviços onde não importa o contexto de continuação.
public class AsyncOptimizedService
{
public async Task<string> GetDataAsync()
{
var result = await GetDataFromDatabaseAsync().ConfigureAwait(false);
return await ProcessDataAsync(result).ConfigureAwait(false);
}
public async Task<IEnumerable<string>> GetMultipleDataAsync()
{
var tasks = Enumerable.Range(1, 10)
.Select(i => GetDataAsync(i));
return await Task.WhenAll(tasks).ConfigureAwait(false);
}
}ValueTask for Performance
O que é: Estrutura que pode representar tanto operações síncronas quanto assíncronas sem alocação de heap.
Como funciona:
ValueTask<T>: Para operações que frequentemente retornam valores síncronosTask<T>: Para operações sempre assíncronas- Evita alocação de objetos quando o resultado está disponível imediatamente
Por que é importante: Reduz pressão no GC e melhora performance para operações que frequentemente completam síncronamente.
Quando usar: Em métodos que frequentemente retornam valores imediatos (cache hits, validações simples).
public class OptimizedService
{
private readonly Dictionary<int, string> _cache = new();
public ValueTask<string> GetDataAsync(int id)
{
if (_cache.TryGetValue(id, out var cached))
return new ValueTask<string>(cached);
return new ValueTask<string>(GetDataFromDatabaseAsync(id));
}
private async Task<string> GetDataFromDatabaseAsync(int id)
{
await Task.Delay(100);
var result = $"Data_{id}";
_cache[id] = result;
return result;
}
}3. LINQ Performance
Optimized LINQ Queries
O que é: Técnicas para otimizar consultas LINQ e reduzir overhead de execução.
Como funciona:
- Deferred Execution: LINQ só executa quando necessário
- Eager Loading: Use
.ToList()quando precisar materializar imediatamente - Projection: Selecione apenas campos necessários
- Filtering Early: Aplique filtros antes de outras operações
Por que é importante: Evita execução desnecessária e reduz uso de memória.
Quando usar: Sempre que trabalhar com grandes coleções ou consultas complexas.
public class LinqOptimizedService
{
private readonly List<User> _users;
public LinqOptimizedService()
{
_users = Enumerable.Range(1, 10000)
.Select(i => new User { Id = i, Name = $"User_{i}", Age = i % 100 })
.ToList();
}
public IEnumerable<User> GetActiveUsersOptimized()
{
return _users.Where(u => u.IsActive)
.Select(u => new { u.Id, u.Name })
.Take(100);
}
public async Task<IEnumerable<User>> GetUsersAsync()
{
return await Task.Run(() => _users.AsParallel()
.Where(u => u.Age > 18)
.OrderBy(u => u.Name)
.ToList());
}
}Compiled Queries
O que é: Consultas pré-compiladas que evitam recompilação a cada execução.
Como funciona:
EF.CompileAsyncQuery: Compila consultas Entity Framework- Armazena planos de execução em cache
- Reduz overhead de parsing e compilação
Por que é importante: Melhora significativamente performance de consultas repetitivas.
Quando usar: Em consultas que são executadas frequentemente com diferentes parâmetros.
public class CompiledQueryService
{
private static readonly Func<ApplicationDbContext, int, Task<User>> GetUserById =
EF.CompileAsyncQuery((ApplicationDbContext context, int id) =>
context.Users.FirstOrDefault(u => u.Id == id));
private static readonly Func<ApplicationDbContext, string, Task<User>> GetUserByEmail =
EF.CompileAsyncQuery((ApplicationDbContext context, string email) =>
context.Users.FirstOrDefault(u => u.Email == email));
public async Task<User> GetUserAsync(int id)
{
using var context = new ApplicationDbContext();
return await GetUserById(context, id);
}
}4. Caching Strategies
Memory Cache
O que é: Cache em memória para armazenar dados frequentemente acessados.
Como funciona:
IMemoryCache: Cache local na memória do processo- Absolute Expiration: Expira em tempo fixo
- Sliding Expiration: Expira após período de inatividade
- Cache Keys: Identificadores únicos para cada item
Por que é importante: Reduz latência e carga no banco de dados para dados acessados frequentemente.
Quando usar: Para dados que não mudam frequentemente e são acessados muitas vezes.
public class CachedUserService : IUserService
{
private readonly IUserService _userService;
private readonly IMemoryCache _cache;
private readonly TimeSpan _cacheExpiration = TimeSpan.FromMinutes(10);
public CachedUserService(IUserService userService, IMemoryCache cache)
{
_userService = userService;
_cache = cache;
}
public async Task<User> GetUserByIdAsync(int id)
{
var cacheKey = $"user_{id}";
if (_cache.TryGetValue(cacheKey, out User cachedUser))
return cachedUser;
var user = await _userService.GetUserByIdAsync(id);
var cacheOptions = new MemoryCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = _cacheExpiration,
SlidingExpiration = TimeSpan.FromMinutes(5)
};
_cache.Set(cacheKey, user, cacheOptions);
return user;
}
}Distributed Cache
O que é: Cache compartilhado entre múltiplas instâncias de aplicação.
Como funciona:
IDistributedCache: Interface para caches distribuídos- Redis: Cache em memória com persistência
- SQL Server: Cache baseado em banco de dados
- Serialização: Converte objetos para string/bytes
Por que é importante: Permite compartilhamento de cache entre múltiplas instâncias e melhora escalabilidade.
Quando usar: Em aplicações com múltiplas instâncias ou quando precisa de persistência de cache.
public class DistributedCachedService : IProductService
{
private readonly IProductService _productService;
private readonly IDistributedCache _cache;
private readonly JsonSerializerOptions _jsonOptions;
public DistributedCachedService(IProductService productService, IDistributedCache cache)
{
_productService = productService;
_cache = cache;
_jsonOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
}
public async Task<IEnumerable<Product>> GetProductsAsync()
{
var cacheKey = "products_all";
var cached = await _cache.GetStringAsync(cacheKey);
if (!string.IsNullOrEmpty(cached))
{
return JsonSerializer.Deserialize<IEnumerable<Product>>(cached, _jsonOptions);
}
var products = await _productService.GetProductsAsync();
var serialized = JsonSerializer.Serialize(products, _jsonOptions);
await _cache.SetStringAsync(cacheKey, serialized, new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30)
});
return products;
}
}5. Database Performance
Connection Pooling
O que é: Reutilização de conexões de banco de dados para reduzir overhead de abertura/fechamento.
Como funciona:
- Pool de Conexões: Mantém conexões abertas e reutilizáveis
- Connection String: Configurações de pool no connection string
- Max Pool Size: Limite máximo de conexões no pool
- Min Pool Size: Número mínimo de conexões mantidas
Por que é importante: Reduz drasticamente latência de operações de banco de dados.
Quando usar: Sempre em aplicações que fazem muitas operações de banco de dados.
public class OptimizedDbContext : DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
optionsBuilder.UseSqlServer(
"Server=localhost;Database=MyDb;Trusted_Connection=true;",
options => options.EnableRetryOnFailure(
maxRetryCount: 3,
maxRetryDelay: TimeSpan.FromSeconds(30),
errorNumbersToAdd: null));
}
}
}Query Optimization
O que é: Técnicas para otimizar consultas de banco de dados e reduzir tempo de execução.
Como funciona:
- AsNoTracking(): Desabilita change tracking para consultas somente leitura
- Include(): Carrega entidades relacionadas em uma consulta
- Select(): Projeta apenas campos necessários
- Indexes: Usa índices apropriados no banco
Por que é importante: Reduz tempo de execução e uso de memória em consultas complexas.
Quando usar: Em consultas que retornam muitos dados ou têm muitas entidades relacionadas.
public class OptimizedRepository<T> : IRepository<T> where T : class
{
private readonly DbContext _context;
public OptimizedRepository(DbContext context)
{
_context = context;
}
public async Task<IEnumerable<T>> GetAllAsync()
{
return await _context.Set<T>()
.AsNoTracking()
.ToListAsync();
}
public async Task<T> GetByIdAsync(int id)
{
return await _context.Set<T>()
.AsNoTracking()
.FirstOrDefaultAsync(e => EF.Property<int>(e, "Id") == id);
}
}6. String Optimization
String Interning
O que é: Técnica para reutilizar instâncias de strings idênticas na memória.
Como funciona:
- String Pool: .NET mantém pool de strings literais
- string.Intern(): Força interning de string
- string.IsInterned(): Verifica se string está no pool
- Memory Efficiency: Reduz uso de memória para strings repetidas
Por que é importante: Reduz uso de memória quando há muitas strings idênticas.
Quando usar: Em aplicações que manipulam muitas strings repetidas (logs, configurações).
public class StringOptimizedService
{
private static readonly string[] CommonNames =
{
"John", "Jane", "Bob", "Alice", "Charlie"
};
public string GetOptimizedName(string name)
{
var internedName = string.Intern(name);
return internedName;
}
public bool IsCommonName(string name)
{
return Array.Exists(CommonNames, n => string.Equals(n, name, StringComparison.OrdinalIgnoreCase));
}
}StringBuilder Usage
O que é: Classe otimizada para construção eficiente de strings grandes.
Como funciona:
- Mutable String: String que pode ser modificada sem criar novas instâncias
- Capacity: Pré-aloca espaço para evitar realocações
- Append Methods: Métodos eficientes para adicionar conteúdo
- Memory Efficiency: Reduz alocações de memória
Por que é importante: Evita criação de múltiplas strings temporárias durante concatenação.
Quando usar: Quando precisa concatenar muitas strings ou construir strings grandes.
public class StringBuilderOptimizedService
{
public string BuildLargeString(IEnumerable<string> items)
{
var sb = new StringBuilder();
foreach (var item in items)
{
sb.AppendLine(item);
}
return sb.ToString();
}
public string BuildFormattedString(int count)
{
var sb = new StringBuilder(count * 10);
for (int i = 0; i < count; i++)
{
sb.AppendFormat("Item {0}: {1}", i, i * 2);
sb.AppendLine();
}
return sb.ToString();
}
}7. Collections Performance
Optimized Collections
O que é: Técnicas para otimizar uso de coleções e reduzir alocações de memória.
Como funciona:
- Capacity: Pré-aloca espaço para evitar redimensionamentos
- Initial Capacity: Define tamanho inicial baseado no uso esperado
- Collection Types: Escolhe tipo apropriado (List vs Array vs HashSet)
- Memory Efficiency: Reduz alocações desnecessárias
Por que é importante: Evita redimensionamentos custosos e melhora performance de operações em coleções.
Quando usar: Quando trabalha com coleções grandes ou que crescem dinamicamente.
public class CollectionOptimizedService
{
public List<int> GetOptimizedList(int capacity)
{
var list = new List<int>(capacity);
for (int i = 0; i < capacity; i++)
{
list.Add(i);
}
return list;
}
public Dictionary<string, int> GetOptimizedDictionary(IEnumerable<KeyValuePair<string, int>> items)
{
var dict = new Dictionary<string, int>(items.Count());
foreach (var item in items)
{
dict[item.Key] = item.Value;
}
return dict;
}
}Concurrent Collections
O que é: Coleções thread-safe para operações concorrentes sem locks explícitos.
Como funciona:
- ConcurrentDictionary: Dictionary thread-safe
- ConcurrentQueue: Fila thread-safe para produtor/consumidor
- ConcurrentBag: Coleção thread-safe sem ordem específica
- Lock-Free: Usa operações atômicas quando possível
Por que é importante: Permite operações concorrentes sem deadlocks e com melhor performance que locks tradicionais.
Quando usar: Em aplicações multi-threaded que compartilham dados entre threads.
public class ConcurrentOptimizedService
{
private readonly ConcurrentDictionary<string, int> _cache = new();
private readonly ConcurrentQueue<string> _queue = new();
public void AddToCache(string key, int value)
{
_cache.TryAdd(key, value);
}
public bool TryGetFromCache(string key, out int value)
{
return _cache.TryGetValue(key, out value);
}
public void EnqueueItem(string item)
{
_queue.Enqueue(item);
}
public bool TryDequeueItem(out string item)
{
return _queue.TryDequeue(out item);
}
}8. Profiling e Monitoring
Performance Counters
O que é: Métricas do sistema operacional para monitorar performance da aplicação.
Como funciona:
- CPU Usage: Percentual de uso do processador
- Memory Usage: Uso de memória disponível
- Disk I/O: Operações de leitura/escrita
- Network I/O: Tráfego de rede
- Custom Counters: Métricas específicas da aplicação
Por que é importante: Permite identificar gargalos e monitorar saúde da aplicação em produção.
Quando usar: Em aplicações críticas que precisam de monitoramento contínuo de performance.
public class PerformanceMonitor
{
private readonly PerformanceCounter _cpuCounter;
private readonly PerformanceCounter _memoryCounter;
public PerformanceMonitor()
{
_cpuCounter = new PerformanceCounter("Processor", "% Processor Time", "_Total");
_memoryCounter = new PerformanceCounter("Memory", "Available MBytes");
}
public float GetCpuUsage()
{
return _cpuCounter.NextValue();
}
public float GetAvailableMemory()
{
return _memoryCounter.NextValue();
}
public void Dispose()
{
_cpuCounter?.Dispose();
_memoryCounter?.Dispose();
}
}Custom Metrics
O que é: Métricas personalizadas para monitorar aspectos específicos da aplicação.
Como funciona:
- Counters: Contadores de eventos (requests, erros)
- Histograms: Distribuição de valores (tempo de resposta)
- Gauges: Valores pontuais (número de conexões ativas)
- Timers: Medição de duração de operações
Por que é importante: Permite monitorar métricas de negócio e identificar problemas específicos da aplicação.
Quando usar: Para monitorar KPIs específicos da aplicação e detectar anomalias.
public class CustomMetrics
{
private readonly Counter _requestCounter;
private readonly Histogram _responseTimeHistogram;
public CustomMetrics()
{
_requestCounter = Metrics.CreateCounter("http_requests_total", "Total HTTP requests");
_responseTimeHistogram = Metrics.CreateHistogram("http_request_duration_seconds", "HTTP request duration");
}
public void RecordRequest(string endpoint)
{
_requestCounter.WithLabels(endpoint).Inc();
}
public IDisposable MeasureRequestDuration()
{
return _responseTimeHistogram.NewTimer();
}
}9. Parallel Processing
Parallel LINQ
O que é: Extensão do LINQ que permite processamento paralelo de coleções.
Como funciona:
- AsParallel(): Habilita processamento paralelo
- WithDegreeOfParallelism(): Controla número de threads
- Partitioning: Divide dados em chunks para processamento
- Thread Safety: Garante operações thread-safe
Por que é importante: Aproveita múltiplos cores para processar grandes volumes de dados mais rapidamente.
Quando usar: Em operações CPU-intensive com grandes coleções de dados.
public class ParallelProcessingService
{
public IEnumerable<int> ProcessDataParallel(IEnumerable<int> data)
{
return data.AsParallel()
.WithDegreeOfParallelism(Environment.ProcessorCount)
.Where(x => x > 0)
.Select(x => x * 2)
.OrderBy(x => x);
}
public async Task<IEnumerable<string>> ProcessDataAsync(IEnumerable<string> data)
{
var tasks = data.Select(async item =>
{
await Task.Delay(100);
return item.ToUpper();
});
return await Task.WhenAll(tasks);
}
}Task Parallel Library
O que é: Biblioteca para programação paralela e assíncrona no .NET.
Como funciona:
- Task.Run(): Executa código em thread pool
- Parallel.ForEach(): Loop paralelo para coleções
- Task.WhenAll(): Aguarda múltiplas tasks
- Cancellation: Suporte a cancelamento de operações
Por que é importante: Permite aproveitar múltiplos cores e melhorar throughput da aplicação.
Quando usar: Em operações que podem ser executadas em paralelo sem dependências entre si.
public class TplOptimizedService
{
public async Task<IEnumerable<int>> ProcessItemsAsync(IEnumerable<int> items)
{
var batches = items.Chunk(1000);
var tasks = batches.Select(ProcessBatchAsync);
var results = await Task.WhenAll(tasks);
return results.SelectMany(x => x);
}
private async Task<IEnumerable<int>> ProcessBatchAsync(IEnumerable<int> batch)
{
return await Task.Run(() => batch.Select(x => x * 2));
}
}10. Memory-Efficient Patterns
Span<T> e Memory<T>
O que é: Tipos que permitem trabalhar com memória de forma eficiente sem alocações de heap.
Como funciona:
- Span<T>: Referência a região de memória contígua
- Memory<T>: Versão heap-allocated do Span
- Stack-Only: Span só pode ser usado na stack
- Zero-Copy: Evita cópias desnecessárias de dados
Por que é importante: Reduz pressão no GC e melhora performance em operações de baixo nível.
Quando usar: Em operações de parsing, serialização ou quando trabalha com buffers de dados.
public class MemoryEfficientService
{
public int SumArray(ReadOnlySpan<int> numbers)
{
var sum = 0;
for (int i = 0; i < numbers.Length; i++)
{
sum += numbers[i];
}
return sum;
}
public string ConcatenateStrings(ReadOnlySpan<char> str1, ReadOnlySpan<char> str2)
{
var result = new char[str1.Length + str2.Length];
str1.CopyTo(result);
str2.CopyTo(result.AsSpan(str1.Length));
return new string(result);
}
}Object Pooling
O que é: Padrão para reutilizar objetos caros em vez de criar/destruir constantemente.
Como funciona:
- Object Pool: Mantém pool de objetos reutilizáveis
- Get/Return: Obtém objeto do pool e retorna após uso
- Reset State: Limpa estado do objeto antes de reutilizar
- Thread Safety: Pool thread-safe para uso concorrente
Por que é importante: Reduz pressão no GC e melhora performance para objetos caros de criar.
Quando usar: Para objetos caros (StringBuilder, DbContext, HttpClient) que são usados frequentemente.
public class ObjectPoolService
{
private readonly ObjectPool<StringBuilder> _stringBuilderPool;
public ObjectPoolService()
{
var provider = new DefaultObjectPoolProvider();
_stringBuilderPool = provider.Create(new StringBuilderPooledObjectPolicy());
}
public string BuildString(IEnumerable<string> items)
{
var sb = _stringBuilderPool.Get();
try
{
foreach (var item in items)
{
sb.AppendLine(item);
}
return sb.ToString();
}
finally
{
_stringBuilderPool.Return(sb);
}
}
}📚 Recursos Adicionais
Ferramentas de Profiling
- Visual Studio Profiler
- dotMemory
- PerfView
- BenchmarkDotNet
Livros Recomendados
- "Writing High-Performance .NET Code" - Ben Watson
- "Pro .NET Performance" - Sasha Goldshtein
⚠️ Pontos de Atenção
- Measure first: Sempre meça antes de otimizar
- Profile in production: Use dados reais para otimização
- Memory leaks: Monitore memory usage
- Async/await: Use adequadamente para I/O operations
- Caching: Use estratégias apropriadas de cache
- Database: Otimize queries e connection pooling