C# - Usando RecyclableMemoryStream
  Neste artigo veremos como usar o recurso RecyblableMemoryStream do C#.


A biblioteca RecyblableMemoryStream do namespace Microsoft.IO fornecer um pool de objetos MemoryStream para melhorar o desempenho do aplicativo, especialmente na área de coleta de lixo.


 

 

Ela é uma biblioteca de alto desempenho projetada para melhorar o desempenho do aplicativo ao trabalhar com streams sendo um substituto para MemoryStream e oferece melhor desempenho do que instâncias MemoryStream.

Podemos usar a biblioteca RecyclableMemoryStream para eliminar alocações de objetos muito grandes na memória -  LOH - Large Objects Heap -  e evitar falhas e fragmentação da memória.

 

A RecyclableMemoryStream melhora o desempenho do Garbage Collector , garantindo que os buffers maiores usados para os streams sejam colocados na heap da geração 2 e permaneçam lá para sempre. Isso deve fazer com que as coletas completas aconteçam com menos frequência. Se você escolher tamanhos de buffer acima de 85.000 bytes, garantirá que eles sejam colocados no heap de objetos grandes, que é acionado com ainda menos frequência pelo coletor de lixo.

 

Para usar os recursos desta biblioteca basta instalar o pacote nuget no seu projeto:

Install-Package Microsoft.IO.RecyclableMemoryStream

A classe RecyclableMemoryStreamManager mantém dois conjuntos separados de objetos:

Nota: Múltiplos Small Pools de 128 KB cada e Large Pools de 1 MB cada (padrão) são criados.

O Large Pool possui duas versões :

  1. O Large Pool Linear que é o padrão e cresce linearmente,

  2. O Large Pool Exponential que cresce de maneira exponencial, ou seja, os buffers dobram de tamanho para cada slot.

Uma instância RecyclableMemoryStream começa alocando inicialmente um pequeno buffer. Buffers adicionais são encadeados à medida que a capacidade do stream aumenta.

Normalmente, o pool pequeno é usado para operações normais de leitura/gravação e o uso de memória é otimizado e eficiente porque os buffers são abstraídos. É por isso que o RecyclableMemoryStream é muito mais eficiente que o MemoryStream.

O pool grande é usado apenas quando o aplicativo precisa de um bloco de memória contíguo.

Quando você chama o método GetBuffer usando o código mostrado abaixo, os buffers pequenos são convertidos em um buffer único, grande e contíguo.

 

var buffer = reciclávelMemoryStreamManager.GetStream().GetBuffer();

Para mais detalhes confira o link: https://github.com/microsoft/Microsoft.IO.RecyclableMemoryStream

 

Usando RecyblableMemoryStream

 

Para mostrar o uso deste recurso vamo Serializar e Zipar um grande objeto que será uma coleção muito grande.

 

Para gerar a coleção de teste vamos usar o Bogus, e vamos usar o código definido em  “The Great C# Example” onde é demonstrado os recursos suportados desta biblioteca. Com base neste exemplo vamos gerar uma coleção de usuários aleatórios que será usada para realizar os benchmarks.

 

Assim vamos criar um projeto do tipo Console no .NET 6 chamado CSharp_RecyclabeMS.

 

A seguir vamos instalar a biblioteca do Bogus :  install-package Bogus

 

Vamos criar a classe ColecaoPessoas no projeto e incluir o código para gerar a coleção fake de pessoas que iremos usar para testar. O código usado aqui voi retirado do exemplo do site oficial do Bogus.

 

O código usado para Serializar e Zipar a coleção pode ser definido da seguinte forma:

 

public async Task SerializeMemoryStream()
{
            using (var stream = new MemoryStream())
            {
                using (var gzip = new GZipStream(stream, CompressionLevel.Optimal, true))
                {
                    await JsonSerializer.SerializeAsync(gzip, _users);
                    await gzip.FlushAsync();
                }
            }
 }

 

Neste código estamos criando um  MemoryStream e um GZipStream e, em seguida, serializamos a coleção de usuários no stream zip e liberamos.

 

Agora vamos realizar a mesma tarefa usando agora RecyclableMemoryStreamManager :

 

private RecyclableMemoryStreamManager _memoryStreamManager;
public async Task SerializeRecyclableMemoryManager()
{
            using (var stream = _memoryStreamManager.GetStream("memory"))
            {
                using (var gzip = new GZipStream(stream, CompressionLevel.Optimal, true))
                {
                    await JsonSerializer.SerializeAsync(gzip, _users);
                    await gzip.FlushAsync();
                }
            }
 }

 

Temos praticamente o mesmo código, exceto que ao invés de usar um MemoryStream estamos fazendo uso do RecyclabeMemoryStreamManager.

 

Para poder testar e comparar o desempenho das duas implementações vamos usar os recursos da biblioteca BenchmarkDotNet.  Assim vamos instalar no projeto o pacote : install-Package BenchmarkDotNet

 

Você pode consultar a documentação deste recurso neste link:  https://fransbouma.github.io/BenchmarkDotNet/GettingStarted.htm

 

Veja também meu artigo sobre o assunto neste link: https://www.macoratti.net/21/11/c_benchmark1.htm

 

Agora vamos criar a classe BenchMark1 no projeto usando o código abaixo:

 

using BenchmarkDotNet.Attributes;
using Microsoft.IO;
using System.IO.Compression;
using System.Text.Json;
namespace CSharp_RecyclableMS;
[MemoryDiagnoser]
public class BenchMark1
{
    private RecyclableMemoryStreamManager? _memoryStreamManager;
    private List<User>? _users;
    [GlobalSetup]
    public void Setup()
    {
        _memoryStreamManager = new RecyclableMemoryStreamManager();
        _users = ColecaoPessoas.Generate();
    }
    [Benchmark]
    public async Task SerializeRecyclableMemoryManager()
    {
        using (var stream = _memoryStreamManager.GetStream("memory"))
        {
            using (var gzip = new GZipStream(stream, CompressionLevel.Optimal, true))
            {
                await JsonSerializer.SerializeAsync(gzip, _users);
                await gzip.FlushAsync();
            }
        }
    }
    [Benchmark]
    public async Task SerializeMemoryStream()
    {
        using (var stream = new MemoryStream())
        {
            using (var gzip = new GZipStream(stream, CompressionLevel.Optimal, true))
            {
                await JsonSerializer.SerializeAsync(gzip, _users);
                await gzip.FlushAsync();
            }
        }
    }
}

 

 

Estamos usando uma coleção de objetos User que contém 8.000 elementos e terá os mesmos dados para ambos os métodos, pois usamos a mesma semente para gerá-los. (Veja o método Generate da classe ColecaoPessoas)

 

Para concluir vamos incluir o  código abaixo na classe Program para iniciar a execução do nosso benckmark:

 

using BenchmarkDotNet.Running;
using CSharp_RecyclableMS;
Console.WriteLine("## Usando RecyclableMemoryStream ##\n");
BenchmarkRunner.Run<BenchMark1>();
Console.ReadKey();

 


Executando o projeto teremos o seguinte resultado :

 


 

Analisando os resultados podemos conferir um melhor desempenho do Garbage Collector na alocação dos objetos na memória.

 

Práticas recomendadas do RecyclableMemoryStream

A fragmentação de memória pode afetar o desempenho de seu aplicativo, e o grande heap de objetos no .NET é propenso à fragmentação. As seguintes diretrizes ou práticas recomendadas devem ser seguidas ao trabalhar com o RecyclableMemoryStream:

O Microsoft.IO.RecyclableMemoryStream é um alocador de streams de memória em pool que é especialista em reduzir a carga de GC e melhorar o desempenho de seus aplicativos, e aproveita os buffers agrupados para eliminar as alocações de heap de objeto grandes (LOH). Ele não apenas evita a fragmentação de memória e vazamentos de memória, mas também fornece métricas que podem ser usadas para rastrear o desempenho.

 

Pegue o projeto aqui : CSharp_RecyclableMS.zip (sem as referências)...

"Não sabeis vós que os que correm no estádio, todos, na verdade, correm, mas um só leva o prêmio? Correi de tal maneira que o alcanceis. E todo aquele que luta de tudo se abstém; eles o fazem para alcançar uma coroa corruptível; nós, porém, uma incorruptível."
1 Coríntios 9:24,25
 

Porque um menino nos nasceu, um filho se nos deu, e o principado está sobre os seus ombros, e se chamará o seu nome: Maravilhoso, Conselheiro, Deus Forte, Pai da Eternidade, Príncipe da Paz.

Isaías 9:6
Porque um menino nos nasceu, um filho se nos deu, e o principado está sobre os seus ombros, e se chamará o seu nome: Maravilhoso, Conselheiro, Deus Forte, Pai da Eternidade, Príncipe da Paz.

Isaías 9:6

Referências:


José Carlos Macoratti