[DotNetBook] Span, Memory y ReadOnlyMemory

Con este artículo, continúo publicando una serie de artículos, cuyo resultado será un libro sobre el trabajo de .NET CLR y .NET en general. Para enlaces, bienvenidos a cat.


Memoria <T> y ReadOnlyMemory <T>


Hay dos diferencias visuales entre Memory<T> y Span<T> . El primero es que el tipo Memory<T> no contiene una restricción de ref en el encabezado del tipo. Es decir, en otras palabras, el tipo Memory<T> tiene derecho a estar no solo en la pila, ya sea una variable local o un parámetro del método o su valor de retorno, sino también en el montón, refiriéndose desde allí a algunos datos en la memoria. Sin embargo, esta pequeña diferencia hace una gran diferencia en el comportamiento y las capacidades de Memory<T> comparación con Span<T> . A diferencia de Span<T> , que es un medio de utilizar un determinado búfer de datos para algunos métodos, el tipo de Memory<T> diseñado para almacenar información sobre el búfer y no para trabajar con él.


Nota


El capítulo publicado en Habré no está actualizado y, probablemente, está un poco desactualizado. Y, por lo tanto, consulte el original para obtener un texto más reciente:



De aquí viene la diferencia en la API:


  • Memory<T> no contiene métodos de acceso a datos que administra. En cambio, tiene la propiedad Span y el método Slice , que devuelve el caballo de batalla, una instancia del tipo Span .
  • Memory<T> contiene adicionalmente el método Pin() , diseñado para secuencias de comandos cuando el búfer almacenado debe pasarse a código unsafe . Cuando se llama para casos en que la memoria se asignó en .NET, el búfer se fijará y no se moverá cuando se MemoryHandle el GC, devolviendo al usuario una instancia de la estructura MemoryHandle , que encapsula el concepto de una vida GCHandle que ha fijado el búfer en la memoria:

 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; } } 

Sin embargo, para empezar, me propongo familiarizarme con todo el conjunto de clases. Y como el primero de ellos, eche un vistazo a la estructura de la Memory<T> (no se muestran todos los miembros de tipo, sino aquellos que parecen ser los 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); } 

Además de especificar los campos de estructura, decidí señalar adicionalmente que hay otros dos constructores de tipos internal que funcionan sobre la base de otra entidad más: el MemoryManager , que se discutirá un poco más y eso no es algo de lo que acabas de hablar. pensamiento: un administrador de memoria en el sentido clásico. Sin embargo, al igual que Span , Memory también contiene una referencia al objeto que se navegará, así como el desplazamiento y el tamaño del búfer interno. Además, vale la pena señalar que la Memory se puede crear con el new operador solo en función de la matriz más los métodos de extensión, en función de la cadena, la matriz y ArraySegment . Es decir su creación sobre la base de memoria no administrada manualmente no está implícita. Sin embargo, como vemos, hay algún método interno para crear esta estructura basado en el 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); } 

Me permitiré discutir un poco con la terminología que se introdujo en el comando CLR, nombrando el tipo con el nombre MemoryManager. Cuando lo vi, primero decidí que sería algo así como una gestión de memoria, pero manual, aparte de LOH / SOH. Pero estaba muy decepcionado de ver la realidad. Quizás debería llamarlo por analogía con la interfaz: MemoryOwner.

Que encapsula el concepto del propietario de una pieza de memoria. En otras palabras, si Span es un medio para trabajar con la memoria, la Memory es un medio para almacenar información sobre un sitio en particular, entonces MemoryManager es un medio para controlar su vida, su propietario. Por ejemplo, puede tomar el tipo NativeMemoryManager<T> , que, aunque escrito para pruebas, no refleja mal la esencia del concepto de "propiedad":


NativeMemoryManager.cs File


 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; } } } } } //   } 

Es decir, en otras palabras, la clase proporciona la posibilidad de llamadas anidadas al método Pin() , contando así los enlaces resultantes del mundo unsafe .


Otra entidad estrechamente relacionada con la Memory es MemoryPool , que proporciona la agrupación de instancias de MemoryManager (y 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() { ... } } 

El cual está diseñado para emitir buffers del tamaño requerido para uso temporal. Las instancias arrendadas que implementan la IMemoryOwner<T> tienen un método Dispose() que devuelve la matriz arrendada de nuevo al grupo de matrices. Y de manera predeterminada, puede usar el grupo de búferes compartido, que se basa 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) { } } 

Y sobre la base de lo que vio, se vislumbra la siguiente imagen del mundo:


  • El tipo de datos Span debe usarse en los parámetros del método si quiere decir leer datos ( ReadOnlySpan ) o escribir ( Span ). Pero no es la tarea de almacenarlo en el campo de clase para uso futuro
  • Si necesita almacenar un enlace al búfer de datos desde el campo de clase, debe usar Memory<T> o ReadOnlyMemory<T> , dependiendo del propósito
  • MemoryManager<T> es el propietario del búfer de datos (no puede usarlo: si es necesario). Es necesario cuando, por ejemplo, es necesario contar las llamadas a Pin() . O cuando necesita tener conocimientos sobre cómo liberar memoria
  • Si la Memory construye alrededor de un área de memoria no administrada, Pin() hará nada. Sin embargo, esto unifica el trabajo con diferentes tipos de buffers: tanto en el caso de código administrado como en el caso de código no administrado, la interfaz de interacción será la misma
  • Cada uno de los tipos tiene constructores públicos. Esto significa que puede usar ambos Span directamente y obtener una copia de la Memory . Puede crear la Memory sí, ya sea por separado o organizar un tipo IMemoryOwner que será el propietario de la porción de memoria a la que hará referencia la Memory . Un caso especial puede ser de cualquier tipo basado en MemoryManager : alguna propiedad local de una pieza de memoria (por ejemplo, con el recuento de referencias de un mundo unsafe ). Si al mismo tiempo necesita extraer dichos búferes (espere tráfico frecuente de búferes de aproximadamente el mismo tamaño), puede usar el tipo MemoryPool .
  • Si está implícito que necesita trabajar con código unsafe , pasando un cierto búfer de datos allí, debe usar el tipo de Memory : tiene un método Pin que automatiza la fijación del búfer en el montón .NET si se creó allí.
  • Si tiene algo de tráfico de búfer (por ejemplo, resuelve el problema de analizar el texto del programa o algún DSL), vale la pena usar el tipo MemoryPool , que se puede organizar de una manera muy correcta, generando búferes del tamaño apropiado del grupo (por ejemplo, un poco más grande si no es adecuado) pero con poda originalMemory.Slice(requiredSize) para no fragmentar el grupo)

Enlace a todo el libro



Source: https://habr.com/ru/post/es420051/


All Articles