[.NET Jogando no Very Hard] Gerenciamento de Memória Parte III
- [.NET Jogando no Very Hard] Gerenciamento de Memória Parte III
Dicas e Ferramentas
Nesta última parte da série de artigos falaremos sobre dicas e truques bastante úteis para nosso dia á dia, conforme eu mesmo for aprendendo técnicas novas vou incrementando essa lista.
Finalizo com todas as referencias utilizadas e os agradecimentos aos revisores técnicos e ortográficos que me auxiliaram na construção destes artigos.
Otimização Prematura
Por muita vezes quando aprendemos algo novo, somos tentados sair correndo implementando estas soluções, porém aqui algumas implementações podem sair bastante caras, veja um exemplo que percebi recentemente, sobrecarregando o GC
public MovementRegisterUseCase : IGenericRegisterFlow
{
private readonly ILogger<MovementRegisterUseCase> _logger;
private readonly IMovementDataGateway _movementDataGateway;
private readonly IConsolidatedMovementProducer _consolidatedMovementProducer;
private List<UnregisteredMovements> _movements;
public MovementRegisterUseCase(
ILogger<MovementRegisterUseCase> logger
IMovementDataGateway movementDataGateway,
IConsolidatedMovementProducer consolidatedMovementProducer)
{
_logger = logger;
_movementDataGateway = movementDataGateway;
_consolidatedMovementProducer = consolidatedMovementProducer;
_movements = new List<string>();
}
public async Task RegisterAsync(CancellationToken cancellationToken = default)
{
var unregisteredMovements = await _movementDataGateway.GetMovementsAsync(cancellationToken);
if (unregisteredMovements.Any())
foreach(var unregisteredMovement in unregisteredMovements)
{
if (unregisteredMovement.CanBeRegistered())
{
_consolidatedMovementProducer.SendAsync(ConsololidatedMovementFactory.Transform(unregisteredMovemen), cancellationToken);
_movements.Add(unregisteredMovement);
}
}
}
~MovementRegisterUseCase()
{
if (_movements.Any())
{
_logger.Debug("{Count} movements registered", _movements.Count());
_movements.Clear();
}
}
}
A principio parece um código simples que varre um repositório e emite eventos de registro de um entidade, porém o destrutor foi erroneamente implementado, devemos lembrar que o destruidor é invocado diretamente pelo GC, e a melhor prática é usa-lo para cenário em que um objeto não gerenciado precisa passar por algum processo de limpeza.
Corrigindo o código
public MovementRegisterUseCase : IGenericRegisterFlow
{
private readonly ILogger<MovementRegisterUseCase> _logger;
private readonly IMovementDataGateway _movementDataGateway;
private readonly IConsolidatedMovementProducer _consolidatedMovementProducer;
private List<UnregisteredMovements> _movements;
public MovementRegisterUseCase(
ILogger<MovementRegisterUseCase> logger
IMovementDataGateway movementDataGateway,
IConsolidatedMovementProducer consolidatedMovementProducer)
{
_logger = logger;
_movementDataGateway = movementDataGateway;
_consolidatedMovementProducer = consolidatedMovementProducer;
_movements = new List<string>();
}
public async Task RegisterAsync(CancellationToken cancellationToken = default)
{
var unregisteredMovements = await _movementDataGateway.GetMovementsAsync(cancellationToken);
if (unregisteredMovements.Any())
foreach(var unregisteredMovement in unregisteredMovements)
{
if (unregisteredMovement.CanBeRegistered())
{
_consolidatedMovementProducer.SendAsync(ConsololidatedMovementFactory.Transform(unregisteredMovemen), cancellationToken);
_movements.Add(unregisteredMovement);
}
}
}
private void ShowResultMovementsRegistered()
{
if (_movements.Any())
{
_logger.Debug("{Count} movements registered", _movements.Count());
_movements.Clear();
}
}
}
Abaixo temos as fotos de antes e depois do ajuste
ANTES

DEPOIS
Perceba que reduzimos a alocação em aproximadamente 24% do espaço que temos para o nosso heap, e que a alocação de objetos classificados em LOH e ou POH reduziu drasticamente, isso ocorreu por que no cenário prévio nosso GC classificou este objeto como LOH devido as excessivas referencias utilizadas na sobrecarga do destruidor, lembre-se
1º) Evite a otimização prematura, comumente o código simples implementado com o devido dispose e ou finalizer está correto!
2º) Quando possível utilize ferramentas com oSonarQube que realize checagem estática do código, e pode identificar facilmente estes problemas.
3º) Você provavelmente não precisa utilizar os data types unsafe como uint, udouble, udecimal etc…
Alocações Desnecessárias
O simples fato de frear a criação excessiva de variáveis já irá contribuir com a redução de espaço da memória Stack e Heap, porém isso não deve sacrificar totalmente a legibilidade de seu código, vamos ao exemplo
var players = new List<Player>(3);
var john = new Player("John", 100);
var jane = new Player("Jane", 200);
var jack = new Player("Jack", 300);
players.Add(john);
players.Add(jane);
players.Add(jack);
Player winner = players.First();
foreach (var player in players)
{
if (player.Score > winner.Score)
{
winner = player;
}
}
public class Player
{
public string? NickName { get; set; }
public double Score { get; set; }
public Player()
{
}
public Player(string nickName, double score)
{
NickName = nickName;
Score = score;
}
}
Correção
var players = new List<Player>
{
new Player("John", 100 ),
new Player("Jane", 200 ),
new Player("Jack", 300 )
};
var winner = players.OrderByDescending(p => p.Score).First();
public class Player
{
public string? NickName { get; set; }
public double Score { get; set; }
public Player()
{
}
public Player(string nickName, double score)
{
NickName = nickName;
Score = score;
}
}
Por diversas vezes as funcionalidades do Linq podem nos apoiar a produzir um código mais simples e com alocação efetiva.
Podemos melhorar ainda mais nosso código a resposta é sim, esse ponto foi um teste benchmark feito pelo amigo Pedro utilizando a lib BenchMarkDotnet, vejamos o código do teste:
[MemoryDiagnoser(true)]
public class PlayerBenchmark
{
[Benchmark(Baseline = true)]
public void SemVariaveis()
{
var players = new List<Player>
{
new Player("John", 100 ),
new Player("Jane", 200 ),
new Player("Jack", 300 )
};
var winner = players.OrderByDescending(p => p.Score).First();
NoOp(winner);
}
[Benchmark]
public void SemVariaveisSemLinq()
{
var players = new List<Player>
{
new Player("John", 100 ),
new Player("Jane", 200 ),
new Player("Jack", 300 )
};
Player winner = players.First();
foreach (var player in players)
{
if (player.Score > winner.Score)
{
winner = player;
}
}
NoOp(winner);
}
[Benchmark]
public void ComVariaveisSemLinq()
{
var players = new List<Player>(3);
var john = new Player("John", 100);
var jane = new Player("Jane", 200);
var jack = new Player("Jack", 300);
players.Add(john);
players.Add(jane);
players.Add(jack);
Player winner = players.First();
foreach (var player in players)
{
if (player.Score > winner.Score)
{
winner = player;
}
}
NoOp(winner);
}
[Benchmark]
public void ComVariaveisComLinq()
{
var players = new List<Player>(3);
var john = new Player("John", 100);
var jane = new Player("Jane", 200);
var jack = new Player("Jack", 300);
players.Add(john);
players.Add(jane);
players.Add(jack);
var winner = players.OrderByDescending(p => p.Score).First();
NoOp(winner);
}
void NoOp(Player player)
{
// No OP
}
}
public class Player
{
public string? NickName { get; set; }
public double Score { get; set; }
public Player()
{
}
public Player(string nickName, double score)
{
NickName = nickName;
Score = score;
}
}
Após executar este observamos que o Linq ainda alocou o dobro de memória que loop sem o Linq, encontrar o ponto de equilibrio entre performance e legibilidade será sempre seu desafio, veja os resultados extraidos do benchmark:

Evite a criação de objetos desnecessários, mais não sacrifique a legibilidade do seu código para isso!!!
Utilize IDisposable para recursos não gerenciados
Sempre fique atento a objetos que são api`s de alto nível para ferramentas como sockets, clientes http, conexões com as bases de dados, api de stream da Microsoft etc…
Todas essas ferramentas implementam a interface IDisposable, na maioria das IDE`s apenas passando o mouse em cima você conseguira observar se a instancia daquele objeto implementa IDisposable.
Se necessário implementar um wrapper para essa funcionalidade, garanta que sua classe implementa o disposable pattern da maneira correta, veja um exemplos
public class DataBaseConnectionWrapper : IDisposable
{
private bool isDisposed = false;
private SqlConnection _sqlConnection
public DataBaseConnectionWrapper(connectionString)
{
_sqlConnection = new SqlConnection(connectionString);
}
~DataBaseConnectionWrapper()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected void Dispose(bool disposing)
{
if (!isDisposed)
{
if (disposing)
{
MyManagedResources.CleanUp();
}
_sqlConnection.Dispose();
isDisposed = true;
}
}
}
Implemente o disposable pattern apenas e quando necessário, segregando os recursos gerenciados dos não gerenciados.
Manipulação de Grandes Textos
Imagine uma string com 5 paginas de textos.
Se possível evite a manipulação em memória de grandes textos, eles são totalmente alocados na memória gerenciada heap, se seu cenário permitir prefira salva-los em arquivos e utilizar as api`s de Stream(exemplo FileStream) da Microsoft, isso pode lhe permitir ler apenas partes especificas necessárias
Span<T>
Span<T> trata-se um novo tipo de valor armazenada de forma continua na memória, ela aproveita todas as vantagens dos dois tipos de memória Stack & Heap, graças ao fato de ser genérica e intermante controlar um Lenght e um Pointer, podemos aproveitar todas as vantagens de acesso que temos em arrays, em objetos do referencia.
Precisamos ter em mente que um Span<T> trata-se apenas de uma visão de memória que promete lhe entregar um acesso mais performático de escrita e leitura de uma região continua.
Vejamos um exemplo
var notas = new int[] { 10, 9, 5, 9, 7, 6 };
var span1 = new Span<int>(notas, 0, 3);
var span2 = new Span<int>(notas, 3, 3);
Console.WriteLine(notas[0]); // será exibido 10
Console.WriteLine(notas[3]); // será exibido 9
Console.WriteLine(span1[0]); // será exibido 10
Console.WriteLine(span2[0]); // será exibido 9
Este tópico é bastante complexo, e pretendo abordar mais em um artigo futuro somente sobre Span<T> e suas vantagens.
TAP Async & Await
Boa parte de nosso dia a dia é escrevendo código com I.O bloqueante utilizando concorrência com async/await
Uma boa prática é garantir que do inicio do seu processo ao final utilizar async/await em todos os pontos que contenham recursos não gerenciados bloqueantes
Task X ValueTask
Em cenários em que você atua com um grande volume de Task`s pode considerar o uso do ***ValueTask*** a principal diferença e o motivo de seu surgimento foi justamente, desafogar a alocação excessiva do objeto Task no GC, por se tratar de uma struct, o mesmo ficará alocado no bloco de memória gerenciada Stack.
E talvez você esteja se perguntando:
Porque não utilizamos ValueTask em tudo?
Simplesmente porque sua principal vantagem ocorre, quando um método pode atuar no thread pool de forma síncrona as na maior parte das vezes, num teste carga sobe um bloco de código utilizando as duas abordagens, você perceberá facilmente se ganho de alocação de memória.
Para este teste recomendo utilizar a lib BenchmarkDotNet
OutOfMemory
Entenda sua exceção *OutOfMemory* ela pode ser lançada principalmente em dois momentos
- Falta de Memória Virtual, heap gerenciado não possui espaço e não conseguiu realizar mais alocação
- Falta de Memória Física, nosso S.O não consegue entregar mais memória para no GC
- Você pode ter um grande espaço em memória sobrando na Gen0, é totalmente comum na maior parte do tempo essa memória ficar mais livre, significa um trabalho efetivo, porém repentinamente objetos grandes em quantidade podem ser alocados, exemplo você sempre recebe listas de 100~10000 objetos e de repente sua lista estou devido a tentar preparar alocação para 1 milhão de objetos
Ferramentas
A principais ferramentas que recomendo para análise de performance e memória são:
- JetBrains dotMemory profiler de memória.
- JetBrains dotTrace performance profiler
- **dotnet-trace** ferramenta oficial microsoft para análise de performance
- **dotnet-counters** ferramenta oficial microsoft para diagnosticos de memória
- **sonar qube** na minha opnião a melhor ferramenta de análise estatica de código, tenha acesso a diversas métricas do código como segurança, complexidade, issues etc…
Conclusão
Espero que esta leitura tenha sido de bom proveito e que lhe ajude em sua carreira, em breve pretendo preparar mais artigos em modelos mais deep dive, afinal todos nós temos uma longa jornada de aprendizado.