
Memoria <T> y ReadOnlyMemory <T>
Hay dos diferencias visuales entre Memory<T>
y Span<T>
. El primero es que el tipo de Memory<T>
no contiene un modificador de ref
en el encabezado del tipo. En otras palabras, el tipo de Memory<T>
puede asignarse tanto en la pila como siendo una variable local, o un parámetro de método, o su valor devuelto y en el montón, haciendo referencia a algunos datos en la memoria desde allí. Sin embargo, esta pequeña diferencia crea una gran distinción en el comportamiento y las capacidades de Memory<T>
comparación con Span<T>
. A diferencia de Span<T>
que es un instrumento para que algunos métodos utilicen algún búfer de datos, el tipo de Memory<T>
está diseñado para almacenar información sobre el búfer, pero no para manejarlo. Por lo tanto, existe la diferencia en API.
Memory<T>
no tiene métodos para acceder a los datos de los que es responsable. En cambio, tiene la propiedad Span
y el método Slice
que devuelve una instancia del tipo Span
.- Además, la
Memory<T>
contiene el método Pin()
utilizado para escenarios en los que los datos almacenados del búfer deben pasarse a unsafe
código unsafe
. Si se llama a este método cuando se asigna memoria en .NET, el búfer se fijará y no se moverá cuando el GC esté activo. Este método devolverá una instancia de la estructura MemoryHandle
, que encapsula GCHandle
para indicar un segmento de una vida útil y para fijar el búfer de matriz en la memoria.
Este capítulo fue traducido del ruso conjuntamente por el autor y por traductores profesionales . Puede ayudarnos con la traducción del ruso o el inglés a cualquier otro idioma, principalmente al chino o al alemán.
Además, si quieres agradecernos, la mejor manera de hacerlo es darnos una estrella en Github o bifurcar el repositorio
github / sidristij / dotnetbook .
Sin embargo, sugiero que nos familiaricemos con todo el conjunto de clases. Primero, echemos un vistazo a la estructura de Memory<T>
sí (aquí solo muestro los miembros tipo que encontré más 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, la estructura contiene el constructor basado en matrices, pero almacena datos en el objeto. Esto es para hacer referencia adicionalmente a cadenas que no tienen un constructor diseñado para ellas, pero que pueden usarse con el método de string
AsMemory()
, devuelve ReadOnlyMemory
. Sin embargo, como ambos tipos deberían ser binarios similares, Object
es el tipo del campo _object
.
A continuación, vemos dos constructores basados en MemoryManager
. Hablaremos de ellos más tarde. Las propiedades de obtener Length
(tamaño) e IsEmpty
comprueban un conjunto vacío. Además, existe el método Slice
para obtener un subconjunto, así como los métodos de copia CopyTo
y TryCopyTo
.
Hablando de Memory
Quiero describir dos métodos de este tipo en detalle: la propiedad Span
y el método Pin
.
Memoria <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; } } }
A saber, las líneas que manejan la gestión de cadenas. Dicen que si convertimos ReadOnlyMemory<T>
a Memory<T>
(estas cosas son iguales en representación binaria e incluso hay un comentario, estos tipos deben coincidir de manera binaria, ya que uno se produce a partir de otro al llamar a Unsafe.As
) tendremos un ~ acceso a una cámara secreta ~ con la oportunidad de cambiar las cadenas. Este es un mecanismo extremadamente peligroso:
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?
Este mecanismo combinado con el internamiento de cuerdas puede producir graves consecuencias.
Memoria <T> .Pin
El segundo método que llama mucho la atención es el 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; }
También es un instrumento importante para la unificación porque si queremos pasar un búfer al código no administrado, solo necesitamos llamar al método Pin()
y pasar un puntero a este código sin importar a qué tipo de datos se refiera la Memory<T>
. Este puntero se almacenará en la propiedad de una estructura resultante.
void PinSample(Memory<byte> memory) { using(var handle = memory.Pin()) { WinApi.SomeApiMethod(handle.Pointer); } }
No importa para qué se solicitó Pin()
en este código: puede ser Memory
que representa T[]
, o una string
o un búfer de memoria no administrada. Simplemente las matrices y la cadena obtendrán un GCHandle.Alloc(array, GCHandleType.Pinned)
real GCHandle.Alloc(array, GCHandleType.Pinned)
y en caso de memoria no administrada no sucederá nada.
MemoryManager, IMemoryOwner, MemoryPool
Además de indicar los campos de estructura, quiero señalar que hay otros dos constructores de tipos internal
basados en otra entidad: MemoryManager
. Este no es un administrador de memoria clásico en el que podría haber pensado y hablaremos más adelante. Administrador de memoria clásico que podría haber pensado y hablaremos de ello más adelante. Al igual que Span
, Memory
tiene una referencia a un objeto navegado, un desplazamiento y un tamaño de un búfer interno. Tenga en cuenta que puede usar el new
operador para crear Memory
desde una matriz. O bien, puede usar métodos de extensión para crear Memory
desde una cadena, una matriz o ArraySegment
. Quiero decir que no está diseñado para crearse manualmente desde la memoria no administrada. Sin embargo, podemos ver que hay un método interno para crear esta estructura usando MemoryManager
.
File 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); }
Esta estructura indica el propietario de un rango de memoria. En otras palabras, Span
es un instrumento para trabajar con memoria, Memory
es una herramienta para almacenar la información sobre un rango de memoria particular y MemoryManager
es una herramienta para controlar la vida útil de este rango, es decir, su propietario. Por ejemplo, podemos ver el tipo NativeMemoryManager<T>
. Aunque se utiliza para pruebas, este tipo representa claramente el concepto de "propiedad".
Archivo 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 }
Eso significa que la clase permite llamadas anidadas del método Pin()
, contando así las referencias generadas del mundo unsafe
.
Otra entidad estrechamente vinculada con la Memory
es MemoryPool
que MemoryManager
instancias de IMemoryOwner
(de hecho, IMemoryOwner
):
File 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() { ... } }
Se utiliza para arrendar tampones de un tamaño necesario para uso temporal. Las instancias alquiladas con la interfaz IMemoryOwner<T>
implementada tienen el método Dispose()
para devolver la matriz alquilada al conjunto de matrices. De forma predeterminada, puede usar el grupo de buffers compartibles creado en ArrayMemoryPool
:
File 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) { } }
Basado en esta arquitectura, tenemos la siguiente imagen:
Span
tipo de datos Span
debe usarse como un parámetro de método si desea leer datos ( ReadOnlySpan
) o leer y escribir datos ( Span
). Sin embargo, no se supone que se almacene en un campo de una clase para uso futuro.- Si necesita almacenar una referencia de un campo de una clase a un búfer de datos, debe usar
Memory<T>
o ReadOnlyMemory<T>
según sus objetivos. MemoryManager<T>
es el propietario de un búfer de datos (opcional). Puede ser necesario si necesita contar llamadas Pin()
, por ejemplo. O, si necesita saber cómo liberar memoria.- Si la
Memory
se construye alrededor de un rango de memoria no administrado, Pin()
no puede hacer nada. Sin embargo, esto funciona con diferentes tipos de buffers: tanto para el código administrado como para el no administrado, la interfaz de interacción será la misma. - Todo tipo tiene constructores públicos. Eso significa que puede usar
Span
directamente u obtener su instancia de Memory
. Para la Memory
como tal, puede crearla individualmente o puede crear un rango de memoria propiedad de IMemoryOwner
y al que hace referencia Memory
. Cualquier tipo basado en MemoryManger
puede considerarse como un caso específico que posee un rango de memoria local (por ejemplo, acompañado de contar las referencias del mundo unsafe
). Además, si necesita agrupar dichos búferes (se espera el tráfico frecuente de búferes de casi el mismo tamaño), puede usar el tipo MemoryPool
. - Si tiene la intención de trabajar con código
unsafe
pasando un búfer de datos allí, debe usar el tipo de Memory
que tiene el método Pin()
que fija automáticamente un búfer en el montón .NET si se creó allí. - Si tiene algo de tráfico de búferes (por ejemplo, analiza un texto de un programa o DSL), es mejor usar el tipo
MemoryPool
. Puede implementarlo adecuadamente para generar los búferes de un tamaño necesario de un grupo (por ejemplo, un búfer ligeramente más grande si no hay uno adecuado, pero usando originalMemory.Slice(requiredSize)
para evitar la fragmentación del grupo).
Para medir el rendimiento de los nuevos tipos de datos, decidí usar una biblioteca que ya se ha convertido en BenchmarkDotNet estándar:
[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")); } }
Ahora, veamos los resultados.

Mirándolos podemos obtener la siguiente información:
ArraySegment
es horrible. Pero si lo envuelve en Span
puede hacerlo menos horrible. En este caso, el rendimiento aumentará 7 veces.- Si consideramos .NET Framework 4.7.1 (lo mismo es para 4.5), el uso de
Span
reducirá significativamente el rendimiento al trabajar con buffers de datos. Disminuirá en un 30–35%. - Sin embargo, si miramos .NET Core 2.1+, el rendimiento sigue siendo similar o incluso aumenta dado que
Span
puede usar una parte de un búfer de datos, creando el contexto. La misma funcionalidad se puede encontrar en ArraySegment
, pero funciona muy lentamente.
Por lo tanto, podemos sacar conclusiones simples sobre el uso de estos tipos de datos:
- para
.NET Framework 4.5+
y .NET Core
tienen la única ventaja: son más rápidos que ArraySegment
cuando se trata de un subconjunto de una matriz original; - en
.NET Core 2.1+
su uso brinda una ventaja innegable sobre ArraySegment
y cualquier implementación manual de Slice
; - Las tres formas son lo más productivas posible y eso no se puede lograr con ninguna herramienta para unificar matrices.
Este capítulo fue traducido del ruso conjuntamente por el autor y por traductores profesionales . Puede ayudarnos con la traducción del ruso o el inglés a cualquier otro idioma, principalmente al chino o al alemán.
