Skip to content

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.

csharp
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 IDisposable para liberar recursos
  • Usa using statement para liberação automática
  • Chama GC.SuppressFinalize para 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.).

csharp
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ível
  • ConfigureAwait(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.

csharp
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íncronos
  • Task<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).

csharp
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.

csharp
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.

csharp
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.

csharp
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.

csharp
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.

csharp
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.

csharp
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).

csharp
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.

csharp
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.

csharp
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.

csharp
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.

csharp
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.

csharp
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.

csharp
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.

csharp
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.

csharp
public class MemoryEfficientService
{
    public int SumArray(ReadOnlySpan&lt;int&gt; numbers)
    {
        var sum = 0;
        for (int i = 0; i &lt; numbers.Length; i++)
        {
            sum += numbers[i];
        }
        return sum;
    }
    
    public string ConcatenateStrings(ReadOnlySpan&lt;char&gt; str1, ReadOnlySpan&lt;char&gt; 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.

csharp
public class ObjectPoolService
{
    private readonly ObjectPool&lt;StringBuilder&gt; _stringBuilderPool;
    
    public ObjectPoolService()
    {
        var provider = new DefaultObjectPoolProvider();
        _stringBuilderPool = provider.Create(new StringBuilderPooledObjectPolicy());
    }
    
    public string BuildString(IEnumerable&lt;string&gt; 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