Com este artigo, continuo publicando uma série de artigos, cujo resultado será um livro sobre o trabalho do .NET CLR e .NET em geral. Para links - bem-vindo ao gato.
Memória <T> e ReadOnlyMemory <T>
Existem duas diferenças visuais entre Memory<T>
e Span<T>
. A primeira é que o tipo de Memory<T>
não contém uma restrição ref
no cabeçalho do tipo. Ou seja, em outras palavras, o tipo Memory<T>
tem o direito de estar não apenas na pilha, seja uma variável local ou um parâmetro do método ou seu valor de retorno, mas também na pilha, referindo-se a partir daí a alguns dados na memória. No entanto, essa pequena diferença faz uma enorme diferença no comportamento e nos recursos da Memory<T>
comparação com o Span<T>
. Diferentemente do Span<T>
, que é um meio de usar um determinado buffer de dados para alguns métodos, o tipo Memory<T>
projetado para armazenar informações sobre o buffer e não para trabalhar com ele.
Nota
O capítulo publicado em Habré não é atualizado e, provavelmente, já está um pouco desatualizado. E, portanto, consulte o original para obter textos mais recentes:

A partir daqui, vem a diferença na API:
Memory<T>
não contém métodos de acesso a dados que gerencia. Em vez disso, ele possui a propriedade Span
e o método Slice
, que retornam o cavalo de trabalho - uma instância do tipo Span
.Memory<T>
contém adicionalmente o método Pin()
, projetado para scripts quando o buffer armazenado deve ser passado para um código unsafe
. Quando é chamado para casos em que a memória foi alocada no .NET, o buffer será fixado e não se moverá quando o GC for acionado, retornando ao usuário uma instância da estrutura MemoryHandle
, que encapsula o conceito de uma vida GCHandle
que corrigiu o buffer na memória:
public unsafe struct MemoryHandle : IDisposable { private void* _pointer; private GCHandle _handle; private IPinnable _pinnable; /// <summary> /// MemoryHandle /// </summary> public MemoryHandle(void* pointer, GCHandle handle = default, IPinnable pinnable = default) { _pointer = pointer; _handle = handle; _pinnable = pinnable; } /// <summary> /// , , /// </summary> [CLSCompliant(false)] public void* Pointer => _pointer; /// <summary> /// _handle _pinnable, /// </summary> public void Dispose() { if (_handle.IsAllocated) { _handle.Free(); } if (_pinnable != null) { _pinnable.Unpin(); _pinnable = null; } _pointer = null; } }
No entanto, para começar, proponho me familiarizar com todo o conjunto de classes. E como o primeiro deles, dê uma olhada na estrutura da própria Memory<T>
(nem todos os membros do tipo são mostrados, mas aqueles que parecem ser os mais importantes):
public readonly struct Memory<T> { private readonly object _object; private readonly int _index, _length; public Memory(T[] array) { ... } public Memory(T[] array, int start, int length) { ... } internal Memory(MemoryManager<T> manager, int length) { ... } internal Memory(MemoryManager<T> manager, int start, int length) { ... } public int Length => _length & RemoveFlagsBitMask; public bool IsEmpty => (_length & RemoveFlagsBitMask) == 0; public Memory<T> Slice(int start, int length); public void CopyTo(Memory<T> destination) => Span.CopyTo(destination.Span); public bool TryCopyTo(Memory<T> destination) => Span.TryCopyTo(destination.Span); }
Além de especificar os campos de estrutura, decidi destacar ainda que existem mais dois construtores de tipo internal
trabalhando com base em mais uma entidade - o MemoryManager
, que será discutido um pouco mais e não é algo que você possa ter apenas pensamento: um gerenciador de memória no sentido clássico. No entanto, como Span
, Memory
também contém uma referência ao objeto que será navegado, bem como o deslocamento e tamanho do buffer interno. Além disso, vale ressaltar que a Memory
pode ser criada com o new
operador apenas com base na matriz e nos métodos de extensão - com base na cadeia, matriz e ArraySegment
. I.e. sua criação com base em memória não gerenciada manualmente não está implícita. No entanto, como vemos, existe algum método interno para criar essa estrutura com base no MemoryManager
:
Arquivo MemoryManager.cs
public abstract class MemoryManager<T> : IMemoryOwner<T>, IPinnable { public abstract MemoryHandle Pin(int elementIndex = 0); public abstract void Unpin(); public virtual Memory<T> Memory => new Memory<T>(this, GetSpan().Length); public abstract Span<T> GetSpan(); protected Memory<T> CreateMemory(int length) => new Memory<T>(this, length); protected Memory<T> CreateMemory(int start, int length) => new Memory<T>(this, start, length); void IDisposable.Dispose() protected abstract void Dispose(bool disposing); }
Permitirei-me discutir um pouco sobre a terminologia que foi introduzida no comando CLR, nomeando o tipo pelo nome MemoryManager. Quando o vi, decidi pela primeira vez que seria algo como um gerenciamento de memória, mas manual, além de LOH / SOH. Mas ele ficou muito decepcionado ao ver a realidade. Talvez você deva chamá-lo por analogia com a interface: MemoryOwner.
O que encapsula o conceito de dono de um pedaço de memória. Em outras palavras, se o Span
for um meio de trabalhar com memória, a Memory
é um meio de armazenar informações sobre um site específico, o MemoryManager
é um meio de controlar sua vida, seu proprietário. Por exemplo, você pode NativeMemoryManager<T>
o tipo NativeMemoryManager<T>
, que, embora escrito para testes, não reflete mal a essência do conceito de "propriedade":
Arquivo NativeMemoryManager.cs
internal sealed class NativeMemoryManager : MemoryManager<byte> { private readonly int _length; private IntPtr _ptr; private int _retainedCount; private bool _disposed; public NativeMemoryManager(int length) { _length = length; _ptr = Marshal.AllocHGlobal(length); } public override void Pin() { ... } public override void Unpin() { lock (this) { if (_retainedCount > 0) { _retainedCount--; if (_retainedCount == 0) { if (_disposed) { Marshal.FreeHGlobal(_ptr); _ptr = IntPtr.Zero; } } } } } // }
Ou seja, em outras palavras, a classe oferece a possibilidade de chamadas aninhadas ao método Pin()
, contando os links resultantes do mundo unsafe
.
Outra entidade intimamente relacionada à Memory
é o MemoryPool
, que fornece um conjunto de instâncias do MemoryManager
(e, de fato, IMemoryOwner
):
Arquivo MemoryPool.cs
public abstract class MemoryPool<T> : IDisposable { public static MemoryPool<T> Shared => s_shared; public abstract IMemoryOwner<T> Rent(int minBufferSize = -1); public void Dispose() { ... } }
Que é projetado para emitir buffers do tamanho necessário para uso temporário. Instâncias IMemoryOwner<T>
que implementam a interface IMemoryOwner<T>
têm um método Dispose()
que retorna a matriz IMemoryOwner<T>
volta ao conjunto de matrizes. E, por padrão, você pode usar o buffer pool compartilhado, construído com base no ArrayMemoryPool
:
Arquivo ArrayMemoryPool.cs
internal sealed partial class ArrayMemoryPool<T> : MemoryPool<T> { private const int MaximumBufferSize = int.MaxValue; public sealed override int MaxBufferSize => MaximumBufferSize; public sealed override IMemoryOwner<T> Rent(int minimumBufferSize = -1) { if (minimumBufferSize == -1) minimumBufferSize = 1 + (4095 / Unsafe.SizeOf<T>()); else if (((uint)minimumBufferSize) > MaximumBufferSize) ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.minimumBufferSize); return new ArrayMemoryPoolBuffer(minimumBufferSize); } protected sealed override void Dispose(bool disposing) { } }
E com base no que ele viu, a seguinte imagem do mundo aparece:
- O tipo de dados
Span
deve ser usado nos parâmetros do método se você quiser ler dados ( ReadOnlySpan
) ou gravar ( Span
). Mas não é a tarefa de armazená-lo no campo de classe para uso futuro - Se você precisar armazenar um link para o buffer de dados do campo da classe, deverá usar a
Memory<T>
ou ReadOnlyMemory<T>
- dependendo da finalidade MemoryManager<T>
é o proprietário do buffer de dados (você não pode usá-lo: se necessário). É necessário quando, por exemplo, é necessário contar chamadas para Pin()
. Ou quando você precisa ter conhecimento sobre como liberar memória- Se a
Memory
construída em torno de uma área de memória não gerenciada, Pin()
fará nada. No entanto, isso unifica o trabalho com diferentes tipos de buffers: no caso de código gerenciado e no de código não gerenciado, a interface de interação será a mesma - Cada um dos tipos possui construtores públicos. Isso significa que você pode usar o
Span
diretamente e obter uma cópia dele da Memory
. Você pode criar a própria Memory
separadamente ou organizar para ela um tipo IMemoryOwner
que possuirá a parte da memória à qual a Memory
fará referência. Um caso especial pode ser de qualquer tipo com base no MemoryManager
: alguma propriedade local de um pedaço de memória (por exemplo, com referência contando de um mundo unsafe
). Se, ao mesmo tempo, você precisar puxar esses buffers (espere tráfego frequente de buffers de tamanho aproximadamente igual), poderá usar o tipo MemoryPool
. - Se estiver implícito que você precisa trabalhar com código
unsafe
, passando um determinado buffer de dados para lá, você deve usar o tipo de Memory
: ele possui um método Pin
que automatiza a correção do buffer no heap do .NET, se um tiver sido criado lá. - Se você tiver algum tráfego de buffer (por exemplo, resolver o problema de analisar o texto do programa ou algum DSL), vale a pena usar o tipo
MemoryPool
, que pode ser organizado de maneira muito correta, gerando buffers do tamanho apropriado do pool (por exemplo, um pouco maior, se não for adequado) mas com remoção originalMemory.Slice(requiredSize)
para não fragmentar o pool)
Link para o livro inteiro
