DEV Community

Cristiano Rodrigues for Unhacked

Posted on

Economize memória e aumente o desempenho com o ArrayPool<T> do .NET

A alocação e desalocação de memória podem impactar negativamente o desempenho de uma aplicação. Especialmente em aplicações de alto desempenho, como servidores web, processamento de big data e jogos, a eficiência no gerenciamento de memória é fundamental para garantir uma execução suave e rápida. Felizmente, o dotnet oferece uma solução poderosa e pouco conhecida para otimizar a alocação de memória: o ArrayPool.

O que é o ArrayPool

O ArrayPool é uma classe que fornece uma maneira eficiente de alocar e reutilizar arrays, evitando a criação e desalocação excessiva de memória. Ele faz parte do namespace System.Buffers e está disponível nas versões do .NET Framework 4.5 e posteriores, bem como no .NET Core e no .NET 5.0 e versões posteriores. É uma classe thread-safe e apesar de sua eficiência comprovada, ainda é pouco utilizado pelos desenvolvedores, o que representa uma grande oportunidade de ganho de desempenho.

Como funciona o ArrayPool?

O ArrayPool gerencia um pool de arrays de um tipo específico T.
Ao invocar o ArrayPool é criado um bucket com 17 arrays com comprimentos de: 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536, 131072, 262144, 524288 e 1048576, conforme podemos observar nos trechos de códigos abaixo:

Shared

TlsOverPerCoreLockedStacksArrayPool

Utilities

A função GetMaxSizeForBucket calcula o tamanho máximo para um "bucket" (compartimento) com base no índice do compartimento, usando um deslocamento de bits à esquerda (<<). O operador de deslocamento de bits à esquerda (<<) desloca os bits de um valor para a esquerda, multiplicando o valor por 2 elevado à potência do número de posições de deslocamento. No caso do código, o valor 16 é deslocado para a esquerda pelo número de posições indicado pelo valor de "binIndex".

A linha "int maxSize = 16 << binIndex;" calcula o tamanho máximo do compartimento, onde o tamanho é igual a 16 multiplicado por 2 elevado à potência do valor de "binIndex". Isso implica que o tamanho do compartimento será multiplicado por 2 a cada incremento de "binIndex". Por exemplo, se "binIndex" for 0, o tamanho máximo será 16 (16 << 0 = 16); se "binIndex" for 1, o tamanho máximo será 32 (16 << 1 = 32); se "binIndex" for 2, o tamanho máximo será 64 (16 << 2 = 64), e assim por diante.

Quando você solicita uma instância do array usando o método Shared, ele retorna uma instância compartilhada. Em seguida, você pode usar o método Rent para obter um buffer que tenha, pelo menos, o comprimento solicitado. E, ao invés de desalocar o array quando não for mais necessário, você o devolve ao ArrayPool chamando o método Return.

A ideia é que, em vez de alocar e desalocar arrays repetidamente durante a execução de uma aplicação, o ArrayPool reutilize arrays previamente alocados sempre que possível. Isso reduz o tempo de execução e o consumo de recursos, especialmente em cenários em que muitos arrays pequenos precisam ser alocados e desalocados rapidamente.

// esqueleto do ArrayPool<T>, onde devemos substituir T pelo tipo

ArrayPool<T> pool = ArrayPool<T>.Shared;
var memory = pool.Rent(20000);
try
{
}
finally
{
  pool.Return(memory);
}
Enter fullscreen mode Exit fullscreen mode

Realizando o Benchmark

Utilizaremos um código muito comum nas aplicações dotnet, onde os desenvolvedores costumam trabalhar com listas para carregarem objetos de uma determinada classe.

Benchmark

Análise do Benchmark

O ArrayPool é mais eficiente trabalhando com arrays maiores, consegue ser 2 vezes mais rápido em alguns casos e com a menor alocação de memória.

Conclusão

O ArrayPool é uma ferramenta poderosa para otimizar a alocação de memória em aplicações de alto desempenho, e seu uso pode levar a uma execução mais suave e rápida. É importante que os desenvolvedores considerem sua utilização em cenários onde a eficiência no gerenciamento de memória é crítica, visando melhorar o desempenho e a escalabilidade de suas aplicações.

Top comments (0)