[DotNetBook]跨度,内存和ReadOnlyMemory

在这篇文章中,我将继续发表一系列文章,其结果将是一本有关.NET CLR和.NET的工作的书。 链接-欢迎来到cat。


内存<T>和ReadOnlyMemory <T>


Memory<T>Span<T>之间有两个视觉差异。 首先是Memory<T>在类型头中不包含ref约束。 也就是说,类型Memory<T>不仅有权在堆栈上(它是方法的本地变量或方法或其返回值的参数),还有权在堆上,从那里引用内存中的某些数据。 但是,与Span<T>相比,此小差异使Memory<T>的行为和功能发生了巨大变化。 与Span<T>不同,后者是对某些方法使用特定数据缓冲区的一种方式,而Memory<T>旨在存储有关缓冲区的信息,而不使用它。


注意事项


在哈布雷(Habré)上发表的这一章没有更新,可能有点过时了。 因此,请转到原始文本以获取更多最新文本:



API的不同之处在于:


  • Memory<T>不包含它管理的数据访问方法。 相反,它具有Span属性和Slice方法,它们返回主力Span类型的实例。
  • Memory<T>还包含Pin()方法,当必须将存储的缓冲区传递给unsafe代码时,该方法专为脚本设计。 当在.NET中分配内存的情况下被调用时,缓冲区将被固定且不会在触发GC时移动,从而向用户返回MemoryHandle结构的实例,该实例封装了已将缓冲区固定在内存中的GCHandle寿命的概念:

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

但是,首先,我建议熟悉整套课程。 首先,看看Memory<T>的结构(未显示所有类型成员,但似乎是最重要的类型成员):


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

除了指定结构字段之外,我还决定指出,还有另外两个internal类型构造函数在另一个实体(即MemoryManager的基础上工作,将进一步讨论,这不是您可能刚刚拥有的东西。思想:经典意义上的记忆管理器。 但是,像Span一样, Memory还包含对将要导航的对象的引用,以及内部缓冲区的偏移量和大小。 另外,值得注意的是,只能使用array和扩展方法-基于字符串,array和ArraySegment ,使用new运算符创建Memory 。 即 不暗示基于非托管内存手动创建它。 但是,正如我们看到的,有一些内部方法可基于MemoryManager创建此结构:


文件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); } 

我将允许自己与CLR命令中引入的术语争论不休,并用MemoryManager名称命名该类型。 当我看到他时,我首先决定这将类似于内存管理,但不是LOH / SOH,而是手动操作。 但是他对看到现实感到非常失望。 也许您应该通过类比来调用它:MemoryOwner。

其中封装了内存所有者的概念。 换句话说,如果Span是使用内存的一种方式, Memory是一种存储有关特定站点的信息的方式,那么MemoryManager就是一种控制其生命(所有者)的方式。 例如,您可以采用NativeMemoryManager<T>类型,该类型尽管是为测试而编写的,但并不能很好地反映“所有权”概念的实质:


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

也就是说,该类提供了对Pin()方法进行嵌套调用的可能性,从而计算了来自unsafe世界的结果链接。


Memory密切相关的另一个实体是MemoryPool ,它提供了MemoryManager实例(实际上是IMemoryOwner )的池:


文件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() { ... } } 

旨在发布临时使用所需大小的缓冲区。 实现IMemoryOwner<T>接口的已租用实例具有Dispose()方法,该方法将租用的阵列返回到阵列池。 并且默认情况下,您可以使用基于ArrayMemoryPool构建的共享缓冲池:


文件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) { } } 

根据他所看到的,世界的以下图片隐约可见:


  • 如果要读取数据( ReadOnlySpan )或写入( Span ),则必须在方法参数中使用Span数据类型。 但不是将其存储在类字段中以供将来使用的任务
  • 如果需要从类字段存储到数据缓冲区的链接,则必须使用Memory<T>ReadOnlyMemory<T> -根据目的
  • MemoryManager<T>是数据缓冲区的所有者(如果需要,您不能使用它)。 例如,当需要计数对Pin()调用时,这是必要的。 或者当您需要有关如何释放内存的知识时
  • 如果在不受管理的内存区域周围建立内存,则Pin()不执行任何操作。 但是,这统一了不同类型的缓冲区的工作:在托管代码的情况下和在非托管代码的情况下,交互接口都是相同的
  • 每种类型都有公共构造函数。 这意味着您可以直接使用两个Span并从Memory获取它的副本。 您可以单独创建Memory本身,也可以为其安排IMemoryOwner类型,该类型将拥有Memory将引用的内存部分。 特殊情况可以是基于MemoryManager任何类型:对内存的某些本地所有权(例如,从不unsafe世界中引用的计数)。 如果同时需要拉出此类缓冲区(请注意缓冲区大小相等的频繁流量),则可以使用MemoryPool类型。
  • 如果暗示您需要使用unsafe代码,在该处传递某个数据缓冲区,则应使用Memory类型:如果在其中创建了一个缓冲区,它具有Pin方法,该方法可以自动在.NET堆中修复缓冲区。
  • 如果您有一些缓冲区流量(例如,您解决了解析程序文本或某些DSL的问题),则值得使用MemoryPool类型,该类型可以以非常正确的方式进行组织,从池中输出适当大小的缓冲区(例如,如果不适合则更大一些)但要修剪originalMemory.Slice(requiredSize)以免碎片化池)

链接到整本书



Source: https://habr.com/ru/post/zh-CN420051/


All Articles