在这篇文章中,我将继续发表一系列文章,其结果将是一本有关.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)
以免碎片化池)
链接到整本书
