C# - Apresentando Memory<T>


  Hoje vou apresentar a struct Memory<T> seus principais métodos e propriedades.

O recurso Memory<T> é uma struct introduzida no C# 7.2 que representa uma região de memória acessível e mutável que oferece uma abstração para trabalhar com dados na memória de forma eficiente, especialmente em cenários de leitura e gravação de grandes quantidades de dados. (O Memory<T> é semelhante ao Span<T>, mas com algumas diferenças importantes.)

Desta forma , Memory<T> é um tipo de referência que representa uma região contígua de memória e tem um comprimento, mas não necessariamente começa no índice 0 e pode ser uma das muitas regiões dentro de outra Memory. A memória representada por Memory pode nem ser do seu próprio processo, pois pode ter sido alocada em código não gerenciado.

Podemos destacar as seguintes características e usos da struct Memory<T> :

  1. Acesso a dados na memória:
    A struct Memory<T> permite acessar e manipular dados em uma região de memória. Ele fornece métodos para leitura e gravação eficientes, sem a necessidade de copiar os dados para outro local.
     
  2. Flexibilidade e segurança:
    Usar Memory<T> é seguro em termos de tipo e permite trabalhar com uma ampla variedade de fontes de dados, como arrays, segmentos de arrays, memória não gerenciada e até mesmo pools de memória. Ele oferece uma maneira flexível de trabalhar com dados, independentemente de sua origem.
     
  3. Reutilização de memória:
    Memory<T> é útil quando você deseja reutilizar a memória de uma operação anterior. Em vez de alocar novos arrays ou buffers a cada vez, você pode criar um Memory<T> que abrange a região de memória existente e reutilizá-lo várias vezes.

A struct Memory<T> possui vários métodos úteis que facilitam a manipulação e o processamento de dados na memória. Aqui estão alguns dos principais métodos:

1 - Slice: Cria uma nova instância de Memory<T> que representa uma subseção contígua do Memory<T> original.
2 -
Span: Obtém um Span<T> que representa o mesmo intervalo de memória do Memory<T>.
3 -
ToArray: Converte o Memory<T> em um array de tipo T
4 -
CopyTo: Copia os elementos do Memory<T> para um Span<T> de destino
5 -
Pin: Fixa o objeto Memory<T> na memória, retornando um MemoryHandle que permite o acesso direto à memória não gerenciada subjacente

Existem também outros métodos, como SequenceEqual, GetEnumerator, GetHashCode, entre outros, que fornecem funcionalidades adicionais para trabalhar com dados na memória de forma eficiente e flexível.

A seguir temos algumas propriedades da struct Memory<T>que fornecem informações sobre a região de memória representada :

  1. Length: Obtém o número de elementos no Memory<T>.
  2. IsEmpty: Indica se o Memory<T> está vazio, ou seja, se o comprimento é igual a zero.
  3. IsEmpty (em C# 10): Indica se o Memory<T> está vazio, ou seja, se o comprimento é igual a
  4. Span: Obtém um Span<T> que representa o mesmo intervalo de memória do Memory<T>.

Tanto  Memory<T> como Span<T> são wrappers sobre buffers de dados estruturados que podem ser usados em pipelines. Assim você deve considerar que os buffers podem ser transmitidos entre APIs e, às vezes, podem ser acessados de várias threads, portanto, esteja ciente de como o tempo de vida de um buffer esta sendo gerenciado.

Agora, embora Span<T> e Memory<T> representem um bloco contíguo de memória, ao contrário de Span<T>, Memory<T> não é uma ref struct.

Portanto, ao contrário do Span<T>, você pode ter Memory<T> em qualquer lugar no heap gerenciado,e assim, você não tem as mesmas restrições em Memory<T> como em Span<T> e pode usar Memory<T> como um campo de classe e além dos limites de await e yield.

Exemplo de uso de Memory<T>

A seguir temos um exemplo básico e simples em um cenário onde você precisa processar um grande arquivo de dados.

Para isso vamos criar um projeto Console App chamado CSharpMemory no ambiente do .NET 7.0 com o seguinte código:

namespace CShapMemory;

public class Program
{
 
static void Main(string[] args)
  {
   
string filePath = "caminho/para/arquivo.bin";

   
using (FileStream fileStream = File.OpenRead(filePath))
    {
     
// Alocar memória para armazenar o conteúdo do arquivo
     
byte[] buffer = new byte[fileStream.Length];

     
// Ler o conteúdo do arquivo para o buffer
      fileStream.Read(buffer, 0, buffer.Length);

      // Criar um Memory<byte> para representar o conteúdo do arquivo
      Memory<
byte> memory = new Memory<byte>(buffer);

     
// Processar os dados
      ProcessarDados(memory);
     }
   }

   static void ProcessarDados(Memory<byte> dataMemory)
   {
     
// Acessar os dados do Memory<byte> como um Span<byte>
      Span<
byte> dataSpan = dataMemory.Span;

     
// Realizar operações de processamento nos dados
     
for (int i = 0; i < dataSpan.Length; i++)
      {
         
// Processar cada byte individualmente
         
byte resultado = ProcessarByte(dataSpan[i]);
         
// Atualizar o byte com o resultado do processamento
          dataSpan[i] = resultado;
      }
     
// Exibir os dados processados
      ExibirDados(dataSpan);
   }

   static byte ProcessarByte(byte dataByte)
   {
     
// Aplicar alguma lógica de processamento ao byte e retornar o resultado
      // Exemplo: Inverter o valor do byte
     
return (byte)~dataByte;
    }

    static void ExibirDados(Span<byte> dataSpan)
    {
      
// Exibir os dados processados
      
foreach (byte value in dataSpan)
       {
          Console.WriteLine(value);
       }
    }
}

No código acima, estamos lendo o conteúdo de um arquivo binário para um Memory<byte>. Isso é feito lendo os bytes do arquivo para um array de bytes usando um FileStream e, em seguida, criando um Memory<byte> a partir desse array.

Em seguida, passamos o Memory<byte> para a função ProcessarDados, que acessa os dados do Memory<byte> como um Span<byte>. Isso permite que você trabalhe diretamente nos bytes do arquivo sem copiá-los para outro local.

Dentro da função ProcessarDados, iteramos sobre os bytes do Span<byte> e aplicamos alguma lógica de processamento, neste caso, invertendo o valor de cada byte. Os bytes processados são armazenados novamente no Span<byte>, atualizando diretamente o Memory<byte>.

Por fim, exibimos os dados processados chamando a função ExibirDados, que itera sobre o Span<byte> e imprime cada byte processado.

Este exemplo ilustra como o Memory<T> pode ser usado para processar grandes quantidades de dados, como em um arquivo, sem a necessidade de copiar ou alocar memória adicional. Ele oferece uma maneira eficiente e flexível de trabalhar com dados na memória.

Nota:  Como sugestão segue o link da apresentação do recurso System.Threading.Channels que permite uma melhor implementação do recurso apresentando neste artigo.

E estamos conversados.

"Eu sou o Alfa e o Ômega, o princípio e o fim, o primeiro e o derradeiro.
Bem-aventurados aqueles que guardam os seus mandamentos, para que tenham direito à árvore da vida, e possam entrar na cidade pelas portas."
Apocalipse 22:13-14

Referências:


José Carlos Macoratti