Partilhar via


MemoryOwner<T>

O MemoryOwner<T> é um tipo de buffer que implementa IMemoryOwner<T>, uma propriedade de comprimento inserido e uma série de APIs orientadas pelo desempenho. É essencialmente um wrapper leve em torno do tipo ArrayPool<T>, com alguns utilitários auxiliares adicionais.

APIs de Plataforma: MemoryOwner<T>, AllocationMode

Como ele funciona

O MemoryOwner<T> tem os seguintes recursos principais:

  • Um dos principais problemas das matrizes retornadas pelas APIs de ArrayPool<T> e das instâncias de IMemoryOwner<T> retornadas pelas APIs de MemoryPool<T> é que o tamanho especificado pelo usuário está sendo usado apenas como um tamanho mínimo: o tamanho real dos buffers retornados pode, na verdade, ser maior. O MemoryOwner<T> resolve isso armazenando também o tamanho original solicitado, de modo que as instâncias de Memory<T> e Span<T> recuperadas dele nunca precisarão ser fatiadas manualmente.
  • Ao usar IMemoryOwner<T>, obter um Span<T> para o buffer subjacente requer primeiro obter uma instância de Memory<T> e, em seguida, um Span<T>. Isso é bastante caro, e muitas vezes desnecessário, pois um Memory<T> intermediário pode não ser necessário. Em vez disso, MemoryOwner<T> tem uma propriedade Span adicional que é extremamente leve, pois encapsula diretamente a matriz T[] interna que está sendo alugada do pool.
  • Os buffers alugados do pool não são limpos por padrão, o que significa que, se não tiverem sido limpos ao serem retornados anteriormente ao pool, poderão conter dados de lixo. Normalmente, os usuários são obrigados a limpar esses buffers alugados manualmente, o que pode ser detalhado especialmente quando feito com frequência. O MemoryOwner<T> tem uma abordagem mais flexível para isso, por meio da API de Allocate(int, AllocationMode). Esse método não apenas aloca uma nova instância exatamente do tamanho solicitado, mas também pode ser usado para especificar qual modo de alocação usar: o mesmo que ArrayPool<T>ou um que limpa automaticamente o buffer alugado.
  • Há casos em que um buffer pode ser alugado com um tamanho maior do que o necessário e ser redimensionado posteriormente. Isso normalmente exigiria que os usuários alugassem um novo buffer e copiassem a região de interesse do buffer antigo. Em vez disso, MemoryOwner<T> expõe uma API de Slice(int, int) que simplesmente retorna uma nova instância encapsulando a área de interesse especificada. Isso permite ignorar o aluguel de um novo buffer e copiar totalmente os itens.

Sintaxe

Aqui está um exemplo de como alugar um buffer e recuperar uma instância de Memory<T>:

// Be sure to include this using at the top of the file:
using Microsoft.Toolkit.HighPerformance.Buffers;

using (MemoryOwner<int> buffer = MemoryOwner<int>.Allocate(42))
{
    // Both memory and span have exactly 42 items
    Memory<int> memory = buffer.Memory;
    Span<int> span = buffer.Span;

    // Writing to the span modifies the underlying buffer
    span[0] = 42;
}

Neste exemplo, usamos um bloco de using para declarar o buffer de MemoryOwner<T>: isso é particularmente útil, pois a matriz subjacente será retornada automaticamente para o pool no final do bloco. Se, em vez disso, não tivermos controle direto sobre o tempo de vida de uma instância de MemoryOwner<T>, o buffer será simplesmente retornado ao pool quando o objeto for finalizado pelo coletor de lixo. Em ambos os casos, os buffers alugados sempre serão retornados corretamente para o pool compartilhado.

Quando isso deve ser usado?

O MemoryOwner<T> pode ser usado como um tipo de buffer de uso geral, que tem a vantagem de minimizar o número de alocações feitas ao longo do tempo, pois reutiliza internamente as mesmas matrizes de um pool compartilhado. Um caso de uso comum é substituir alocações de matriz de new T[], principalmente ao fazer operações repetidas que exigem um buffer temporário para trabalhar ou que produzem um buffer como resultado.

Suponha que tenhamos um conjunto de dados que consiste em uma série de arquivos binários e que precisamos ler todos esses arquivos e processá-los de alguma forma. Para separar corretamente nosso código, podemos acabar escrevendo um método que simplesmente lê um arquivo binário, que pode ter esta aparência:

public static byte[] GetBytesFromFile(string path)
{
    using Stream stream = File.OpenRead(path);

    byte[] buffer = new byte[(int)stream.Length];

    stream.Read(buffer, 0, buffer.Length);

    return buffer;
}

Observe essa expressão new byte[]. Se lermos um grande número de arquivos, acabaremos alocando muitas novas matrizes, o que colocará muita pressão sobre o coletor de lixo. Talvez queiramos refatorar esse código usando buffers alugados de um pool, da seguinte maneira:

public static (byte[] Buffer, int Length) GetBytesFromFile(string path)
{
    using Stream stream = File.OpenRead(path);

    byte[] buffer = ArrayPool<T>.Shared.Rent((int)stream.Length);

    stream.Read(buffer, 0, (int)stream.Length);

    return (buffer, (int)stream.Length);
}

Usando essa abordagem, os buffers agora são alugados de um pool, o que significa que, na maioria dos casos, podemos ignorar uma alocação. Além disso, como os buffers alugados não são limpos por padrão, também podemos economizar o tempo necessário para preenchê-los com zeros, o que nos dá outra pequena melhoria de desempenho. No exemplo acima, carregar 1.000 arquivos levaria o tamanho total da alocação de cerca de 1 MB para apenas 1024 bytes – apenas um único buffer seria efetivamente alocado e reutilizado automaticamente.

Há dois problemas principais com o código acima:

  • O ArrayPool<T> pode retornar buffers que têm um tamanho maior que o solicitado. Para solucionar esse problema, precisamos retornar uma tupla que também indica o tamanho real usado em nosso buffer alugado.
  • Ao simplesmente retornar uma matriz, precisamos ter mais cuidado para acompanhar corretamente seu tempo de vida e devolvê-la ao pool apropriado. Podemos contornar esse problema usando MemoryPool<T> em vez disso e retornando uma instância de IMemoryOwner<T>, mas ainda temos o problema de buffers alugados terem um tamanho maior do que o necessário. Além disso, IMemoryOwner<T> tem alguma sobrecarga ao recuperar um Span<T> para trabalhar, por ser uma interface e o fato de que sempre precisamos obter uma instância de Memory<T> primeiro e, em seguida, uma Span<T>.

Para resolver esses dois problemas, podemos refatorar esse código novamente usando MemoryOwner<T>:

public static MemoryOwner<byte> GetBytesFromFile(string path)
{
    using Stream stream = File.OpenRead(path);

    MemoryOwner<byte> buffer = MemoryOwner<byte>.Allocate((int)stream.Length);

    stream.Read(buffer.Span);

    return buffer;
}

A instância de IMemoryOwner<byte> retornada vai descartar o buffer subjacente e devolvê-lo ao pool quando seu método IDisposable.Dispose for invocado. Podemos usá-lo para obter uma instância de Memory<T> ou Span<T> para interagir com os dados carregados e descartar a instância quando não precisarmos mais dela. Além disso, todas as propriedades de MemoryOwner<T> (como MemoryOwner<T>.Span) respeitam o tamanho inicial solicitado que usamos, portanto, não precisamos mais controlar manualmente o tamanho efetivo dentro do buffer alugado.

Exemplos

Você pode encontrar mais exemplos nos testes de unidade.