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 deIMemoryOwner<T>
retornadas pelas APIs deMemoryPool<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. OMemoryOwner<T>
resolve isso armazenando também o tamanho original solicitado, de modo que as instâncias deMemory<T>
eSpan<T>
recuperadas dele nunca precisarão ser fatiadas manualmente. - Ao usar
IMemoryOwner<T>
, obter umSpan<T>
para o buffer subjacente requer primeiro obter uma instância deMemory<T>
e, em seguida, umSpan<T>
. Isso é bastante caro, e muitas vezes desnecessário, pois umMemory<T>
intermediário pode não ser necessário. Em vez disso,MemoryOwner<T>
tem uma propriedadeSpan
adicional que é extremamente leve, pois encapsula diretamente a matrizT[]
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 deAllocate(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 queArrayPool<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 deSlice(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 deIMemoryOwner<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 umSpan<T>
para trabalhar, por ser uma interface e o fato de que sempre precisamos obter uma instância deMemory<T>
primeiro e, em seguida, umaSpan<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.
.NET Community Toolkit