
Memória <T> e ReadOnlyMemory <T>
Existem duas diferenças visuais entre Memory<T>
e Span<T>
. O primeiro é que o tipo de Memory<T>
não contém modificador ref
no cabeçalho do tipo. Em outras palavras, o tipo Memory<T>
pode ser alocado na pilha enquanto é uma variável local ou um parâmetro de método, ou seu valor retornado e no heap, referenciando alguns dados na memória a partir daí. No entanto, essa pequena diferença cria uma enorme distinção no comportamento e nos recursos da Memory<T>
comparação com o Span<T>
. Diferentemente do Span<T>
que é um instrumento para alguns métodos usarem um buffer de dados, o tipo Memory<T>
foi projetado para armazenar informações sobre o buffer, mas não para manipulá-lo. Assim, há a diferença na API.
Memory<T>
não possui métodos para acessar os dados pelos quais é responsável. Em vez disso, ele possui a propriedade Span
e o método Slice
que retornam uma instância do tipo Span
.- Além disso, a
Memory<T>
contém o método Pin()
usado para cenários em que os dados armazenados no buffer devem ser passados para código unsafe
. Se esse método for chamado quando a memória for alocada no .NET, o buffer será fixado e não se moverá quando o GC estiver ativo. Este método retornará uma instância da estrutura MemoryHandle
, que encapsula o GCHandle
para indicar um segmento de uma vida útil e fixar o buffer da matriz na memória.
Este capítulo foi traduzido do russo em conjunto pelo autor e por tradutores profissionais . Você pode nos ajudar com a tradução do russo ou do inglês para qualquer outro idioma, principalmente para chinês ou alemão.
Além disso, se você quiser nos agradecer, a melhor maneira de fazer isso é nos dar uma estrela no github ou no fork do repositório
github / sidristij / dotnetbook .
No entanto, sugiro que nos familiarizemos com todo o conjunto de classes. Primeiro, vejamos a própria estrutura Memory<T>
(aqui mostro apenas os membros do tipo que achei 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); public Span<T> Span { get; } public unsafe MemoryHandle Pin(); }
Como vemos, a estrutura contém o construtor com base em matrizes, mas armazena dados no objeto. Isso serve para referenciar adicionalmente seqüências de caracteres que não possuem um construtor projetado para elas, mas que podem ser usadas com o método de string
AsMemory()
, ele retorna ReadOnlyMemory
. No entanto, como os dois tipos devem ser binários semelhantes, Object
é o tipo do campo _object
.
A seguir, vemos dois construtores baseados no MemoryManager
. Falaremos sobre eles mais tarde. As propriedades de obtenção de Length
(size) e IsEmpty
verificam se há um conjunto vazio. Além disso, existe o método Slice
para obter um subconjunto, bem como os métodos CopyTo
e TryCopyTo
de cópia.
Falando sobre Memory
Quero descrever em detalhes dois métodos desse tipo: a propriedade Span
e o método Pin
.
Memória <T> .Span
public Span<T> Span { get { if (_index < 0) { return ((MemoryManager<T>)_object).GetSpan().Slice(_index & RemoveFlagsBitMask, _length); } else if (typeof(T) == typeof(char) && _object is string s) { // This is dangerous, returning a writable span for a string that should be immutable. // However, we need to handle the case where a ReadOnlyMemory<char> was created from a string // and then cast to a Memory<T>. Such a cast can only be done with unsafe or marshaling code, // in which case that's the dangerous operation performed by the dev, and we're just following // suit here to make it work as best as possible. return new Span<T>(ref Unsafe.As<char, T>(ref s.GetRawStringData()), s.Length).Slice(_index, _length); } else if (_object != null) { return new Span<T>((T[])_object, _index, _length & RemoveFlagsBitMask); } else { return default; } } }
Ou seja, as linhas que lidam com o gerenciamento de strings. Eles dizem que, se convertermos ReadOnlyMemory<T>
em Memory<T>
(essas coisas são iguais na representação binária e há até um comentário, esses tipos devem coincidir de maneira binária, à medida que um é produzido a partir de outro, chamando Unsafe.As
) teremos ~ acesso a uma câmara secreta ~ com a oportunidade de mudar as cordas. Este é um mecanismo extremamente perigoso:
unsafe void Main() { var str = "Hello!"; ReadOnlyMemory<char> ronly = str.AsMemory(); Memory<char> mem = (Memory<char>)Unsafe.As<ReadOnlyMemory<char>, Memory<char>>(ref ronly); mem.Span[5] = '?'; Console.WriteLine(str); } --- Hello?
Esse mecanismo combinado com o internamento de strings pode produzir consequências terríveis.
Memória <T> .Pin
O segundo método que chama muita atenção é o Pin
:
public unsafe MemoryHandle Pin() { if (_index < 0) { return ((MemoryManager<T>)_object).Pin((_index & RemoveFlagsBitMask)); } else if (typeof(T) == typeof(char) && _object is string s) { // This case can only happen if a ReadOnlyMemory<char> was created around a string // and then that was cast to a Memory<char> using unsafe / marshaling code. This needs // to work, however, so that code that uses a single Memory<char> field to store either // a readable ReadOnlyMemory<char> or a writable Memory<char> can still be pinned and // used for interop purposes. GCHandle handle = GCHandle.Alloc(s, GCHandleType.Pinned); void* pointer = Unsafe.Add<T>(Unsafe.AsPointer(ref s.GetRawStringData()), _index); return new MemoryHandle(pointer, handle); } else if (_object is T[] array) { // Array is already pre-pinned if (_length < 0) { void* pointer = Unsafe.Add<T>(Unsafe.AsPointer(ref array.GetRawSzArrayData()), _index); return new MemoryHandle(pointer); } else { GCHandle handle = GCHandle.Alloc(array, GCHandleType.Pinned); void* pointer = Unsafe.Add<T>(Unsafe.AsPointer(ref array.GetRawSzArrayData()), _index); return new MemoryHandle(pointer, handle); } } return default; }
Também é um instrumento importante para a unificação porque, se queremos passar um buffer para código não gerenciado, precisamos chamar o método Pin()
e passar um ponteiro para esse código, independentemente do tipo de dados referente à Memory<T>
. Este ponteiro será armazenado na propriedade de uma estrutura resultante.
void PinSample(Memory<byte> memory) { using(var handle = memory.Pin()) { WinApi.SomeApiMethod(handle.Pointer); } }
Não importa qual Pin()
foi solicitado nesse código: pode ser Memory
que representa T[]
ou uma string
ou um buffer de memória não gerenciada. Apenas matrizes e cadeias obterão um GCHandle.Alloc(array, GCHandleType.Pinned)
real GCHandle.Alloc(array, GCHandleType.Pinned)
e, no caso de memória não gerenciada, nada acontecerá.
MemoryManager, IMemoryOwner, MemoryPool
Além de indicar os campos da estrutura, quero observar que existem outros dois construtores de tipo internal
baseados em outra entidade - MemoryManager
. Este não é um gerenciador de memória clássico em que você possa ter pensado e falaremos sobre isso mais tarde. gerenciador de memória clássico que você pode ter pensado e falaremos sobre isso mais tarde. Como Span
, Memory
tem uma referência a um objeto navegado, um deslocamento e um tamanho de um buffer interno. Observe que você pode usar o new
operador para criar Memory
de uma matriz. Ou, você pode usar métodos de extensão para criar Memory
partir de uma string, uma matriz ou ArraySegment
. Quero dizer, ele não foi projetado para ser criado manualmente a partir da memória não gerenciada. No entanto, podemos ver que existe um método interno para criar essa estrutura usando o 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); }
Essa estrutura indica o proprietário de um intervalo de memória. Em outras palavras, o Span
é um instrumento para trabalhar com memória, a Memory
é uma ferramenta para armazenar as informações sobre um determinado intervalo de memória e o MemoryManager
é uma ferramenta para controlar a vida útil desse intervalo, ou seja, seu proprietário. Por exemplo, podemos olhar para o tipo NativeMemoryManager<T>
. Embora seja usado para testes, esse tipo representa claramente o 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; } } } } } // Other methods }
Isso significa que a classe permite chamadas aninhadas do método Pin()
, contando, assim, referências geradas do mundo unsafe
.
Outra entidade intimamente ligada à Memory
é o MemoryPool
que agrupa instâncias do MemoryManager
( IMemoryOwner
de fato):
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() { ... } }
É usado para alugar buffers do tamanho necessário para uso temporário. As instâncias IMemoryOwner<T>
com a interface IMemoryOwner<T>
implementada têm o método Dispose()
para retornar a matriz IMemoryOwner<T>
volta ao conjunto de matrizes. Por padrão, você pode usar o pool compartilhável de buffers criados 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) { } }
Com base nessa arquitetura, temos a seguinte imagem:
Span
tipo de dados Span
deve ser usado como parâmetro de método se você quiser ler dados ( ReadOnlySpan
) ou ler e gravar dados ( Span
). No entanto, não deve ser armazenado em um campo de uma classe para uso futuro.- Se você precisar armazenar uma referência de um campo de uma classe em um buffer de dados, precisará usar a
Memory<T>
ou ReadOnlyMemory<T>
dependendo dos seus objetivos. MemoryManager<T>
é o proprietário de um buffer de dados (opcional). Pode ser necessário se você precisar contar as chamadas Pin()
, por exemplo. Ou, se você precisar saber como liberar memória.- Se a
Memory
for criada em torno de um intervalo de memória não gerenciada, Pin()
não poderá fazer nada. No entanto, isso unifica o trabalho com diferentes tipos de buffers: para código gerenciado e não gerenciado, a interface de interação será a mesma. - Todo tipo tem construtores públicos. Isso significa que você pode usar o
Span
diretamente ou obter sua instância da Memory
. Para a Memory
como tal, você pode criá-lo individualmente ou criar um intervalo de memória pertencente ao IMemoryOwner
e referenciado pelo Memory
. Qualquer tipo baseado no MemoryManger
pode ser considerado um caso específico do qual possui algum intervalo de memória local (por exemplo, acompanhado pela contagem de referências do mundo unsafe
). Além disso, se você precisar agrupar esses buffers (o tráfego frequente de buffers de tamanho quase igual é esperado), poderá usar o tipo MemoryPool
. - Se você pretende trabalhar com código
unsafe
, passando um buffer de dados para lá, use o tipo de Memory
que possui o método Pin()
que fixa automaticamente um buffer no heap do .NET, se ele foi criado lá. - Se você tiver algum tráfego de buffers (por exemplo, analisa o texto de um programa ou DSL), é melhor usar o tipo
MemoryPool
. Você pode implementá-lo adequadamente para gerar os buffers do tamanho necessário de um pool (por exemplo, um buffer um pouco maior se não houver um adequado, mas usando originalMemory.Slice(requiredSize)
para evitar a fragmentação do pool).
Para medir o desempenho de novos tipos de dados, decidi usar uma biblioteca que já se tornou padrão BenchmarkDotNet :
[Config(typeof(MultipleRuntimesConfig))] public class SpanIndexer { private const int Count = 100; private char[] arrayField; private ArraySegment<char> segment; private string str; [GlobalSetup] public void Setup() { str = new string(Enumerable.Repeat('a', Count).ToArray()); arrayField = str.ToArray(); segment = new ArraySegment<char>(arrayField); } [Benchmark(Baseline = true, OperationsPerInvoke = Count)] public int ArrayIndexer_Get() { var tmp = 0; for (int index = 0, len = arrayField.Length; index < len; index++) { tmp = arrayField[index]; } return tmp; } [Benchmark(OperationsPerInvoke = Count)] public void ArrayIndexer_Set() { for (int index = 0, len = arrayField.Length; index < len; index++) { arrayField[index] = '0'; } } [Benchmark(OperationsPerInvoke = Count)] public int ArraySegmentIndexer_Get() { var tmp = 0; var accessor = (IList<char>)segment; for (int index = 0, len = accessor.Count; index < len; index++) { tmp = accessor[index]; } return tmp; } [Benchmark(OperationsPerInvoke = Count)] public void ArraySegmentIndexer_Set() { var accessor = (IList<char>)segment; for (int index = 0, len = accessor.Count; index < len; index++) { accessor[index] = '0'; } } [Benchmark(OperationsPerInvoke = Count)] public int StringIndexer_Get() { var tmp = 0; for (int index = 0, len = str.Length; index < len; index++) { tmp = str[index]; } return tmp; } [Benchmark(OperationsPerInvoke = Count)] public int SpanArrayIndexer_Get() { var span = arrayField.AsSpan(); var tmp = 0; for (int index = 0, len = span.Length; index < len; index++) { tmp = span[index]; } return tmp; } [Benchmark(OperationsPerInvoke = Count)] public int SpanArraySegmentIndexer_Get() { var span = segment.AsSpan(); var tmp = 0; for (int index = 0, len = span.Length; index < len; index++) { tmp = span[index]; } return tmp; } [Benchmark(OperationsPerInvoke = Count)] public int SpanStringIndexer_Get() { var span = str.AsSpan(); var tmp = 0; for (int index = 0, len = span.Length; index < len; index++) { tmp = span[index]; } return tmp; } [Benchmark(OperationsPerInvoke = Count)] public void SpanArrayIndexer_Set() { var span = arrayField.AsSpan(); for (int index = 0, len = span.Length; index < len; index++) { span[index] = '0'; } } [Benchmark(OperationsPerInvoke = Count)] public void SpanArraySegmentIndexer_Set() { var span = segment.AsSpan(); for (int index = 0, len = span.Length; index < len; index++) { span[index] = '0'; } } } public class MultipleRuntimesConfig : ManualConfig { public MultipleRuntimesConfig() { Add(Job.Default .With(CsProjClassicNetToolchain.Net471) // Span not supported by CLR .WithId(".NET 4.7.1")); Add(Job.Default .With(CsProjCoreToolchain.NetCoreApp20) // Span supported by CLR .WithId(".NET Core 2.0")); Add(Job.Default .With(CsProjCoreToolchain.NetCoreApp21) // Span supported by CLR .WithId(".NET Core 2.1")); Add(Job.Default .With(CsProjCoreToolchain.NetCoreApp22) // Span supported by CLR .WithId(".NET Core 2.2")); } }
Agora, vamos ver os resultados.

Olhando para eles, podemos obter as seguintes informações:
ArraySegment
é horrível. Mas se você envolvê-lo no Span
poderá torná-lo menos horrível. Nesse caso, o desempenho aumentará 7 vezes.- Se considerarmos o .NET Framework 4.7.1 (o mesmo vale para 4.5), o uso do
Span
diminuirá significativamente o desempenho ao trabalhar com buffers de dados. Diminuirá de 30 a 35%. - No entanto, se observarmos o .NET Core 2.1+, o desempenho permanecerá semelhante ou até aumentará, pois o
Span
pode usar parte de um buffer de dados, criando o contexto. A mesma funcionalidade pode ser encontrada no ArraySegment
, mas funciona muito lentamente.
Assim, podemos tirar conclusões simples sobre o uso desses tipos de dados:
- para
.NET Framework 4.5+
e .NET Core
eles têm a única vantagem: são mais rápidos que ArraySegment
ao lidar com um subconjunto de uma matriz original; - no
.NET Core 2.1+
seu uso oferece uma vantagem inegável sobre o ArraySegment
e qualquer implementação manual do Slice
; - todas as três maneiras são tão produtivas quanto possível e isso não pode ser alcançado com nenhuma ferramenta para unificar matrizes.
Este capítulo foi traduzido do russo em conjunto pelo autor e por tradutores profissionais . Você pode nos ajudar com a tradução do russo ou do inglês para qualquer outro idioma, principalmente para chinês ou alemão.
