[DotNetBook] Span ، Memory ، و ReadOnlyMemory

مع هذه المقالة ، أواصل نشر سلسلة من المقالات ، ستكون نتائجه كتابًا عن عمل .NET CLR و .NET بشكل عام. للروابط - مرحبا بك في القط.


الذاكرة <T> و ReadOnlyMemory <T>


هناك اختلافان مرئيان بين Memory<T> و Span<T> . الأول هو أن نوع Memory<T> لا يحتوي على قيد ref في رأس النوع. بمعنى آخر ، نوع Memory<T> لها الحق في أن تكون ليس فقط على المكدس ، إما أن تكون متغيرًا محليًا أو معلمة للطريقة أو قيمتها المرتجعة ، ولكن أيضًا في الكومة ، تشير من هناك إلى بعض البيانات في الذاكرة. ومع ذلك ، فإن هذا الاختلاف الصغير يحدث فرقًا كبيرًا في سلوك وقدرات Memory<T> مقارنةً Span<T> . على عكس Span<T> ، وهو وسيلة لاستخدام مخزن بيانات مؤقت لبعض الطرق ، Memory<T> تصميم Memory<T> لتخزين المعلومات حول المخزن المؤقت ، وليس للعمل معه.


ملاحظة


لم يتم تحديث الفصل المنشور على حبري ، وربما يكون قديمًا بالفعل. وبالتالي ، يرجى الرجوع إلى النص الأصلي للحصول على نص أحدث:



من هنا يأتي الاختلاف في API:


  • لا تحتوي Memory<T> على طرق الوصول إلى البيانات التي تديرها. بدلاً من ذلك ، يحتوي على خاصية Span وأسلوب Slice ، اللذين يعيدان حصان العمل - مثيل من نوع Span .
  • بالإضافة إلى ذلك ، تحتوي Memory<T> على طريقة Pin() المصممة للبرامج النصية عندما يجب تمرير المخزن المؤقت المخزن إلى رمز unsafe . عندما يتم استدعاؤها للحالات التي تم فيها تخصيص الذاكرة في .NET ، سيتم تثبيت المخزن المؤقت ولن يتحرك عندما يتم تشغيل GC ، مما يعيد إلى المستخدم MemoryHandle بنية 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 النوع internal يعملان على أساس كيان آخر - MemoryManager ، والذي سيتم مناقشته أكثر قليلاً وهذا ليس شيئًا قد تحدثت عنه للتو الفكر: مدير ذاكرة بالمعنى الكلاسيكي. ومع ذلك ، مثل Span ، تحتوي Memory أيضًا على مرجع إلى الكائن الذي سيتم التنقل فيه ، بالإضافة إلى إزاحة وحجم المخزن المؤقت الداخلي. من الجدير بالذكر أيضًا أنه يمكن إنشاء Memory باستخدام المشغل new فقط على أساس طرق الصفيف بالإضافة إلى التمديد - على أساس السلسلة ArraySegment و ArraySegment . على سبيل المثال لا يعني ضمناً إنشائه على أساس الذاكرة غير المُدارة. ومع ذلك ، كما نرى ، هناك بعض الطرق الداخلية لإنشاء هذه البنية استنادًا إلى 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 ، والذي يوفر 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) { } } 

وبناءً على ما رآه ، تلوح صورة العالم التالية:


  • يجب استخدام نوع بيانات Span في معلمات الطريقة إذا كنت تقصد إما قراءة البيانات ( ReadOnlySpan ) أو الكتابة ( Span ). ولكن ليس مهمة تخزينها في مجال الصف لاستخدامها في المستقبل
  • إذا كنت بحاجة إلى تخزين ارتباط بمخزن البيانات المؤقت من حقل الفئة ، فيجب استخدام Memory<T> أو ReadOnlyMemory<T> - اعتمادًا على الغرض
  • MemoryManager<T> هو مالك مخزن البيانات المؤقت (لا يمكنك استخدامه: إذا لزم الأمر). من الضروري ، على سبيل المثال ، عند الحاجة إلى حساب المكالمات Pin() . أو عندما تحتاج إلى معرفة كيفية تحرير الذاكرة
  • إذا Memory بناء Memory حول منطقة ذاكرة غير مُدارة ، Pin() يقوم Pin() بأي شيء. ومع ذلك ، فإن هذا يوحد العمل مع أنواع مختلفة من المخازن المؤقتة: في حالة التعليمات البرمجية المُدارة وفي حالة التعليمات البرمجية غير المُدارة ، ستكون واجهة التفاعل هي نفسها
  • كل نوع من الأنواع له منشئات عامة. هذا يعني أنه يمكنك استخدام كل من Span مباشرة والحصول على نسخة منه من Memory . يمكنك إنشاء Memory نفسها إما بشكل منفصل أو ترتيب نوع من نوع IMemoryOwner يمتلك جزء الذاكرة الذي IMemoryOwner إليه Memory . قد تكون حالة خاصة من أي نوع استنادًا إلى MemoryManager : بعض الملكية المحلية لقطعة من الذاكرة (على سبيل المثال ، مع حساب مرجعي من عالم unsafe ). إذا كنت بحاجة في نفس الوقت إلى سحب مثل هذه المخازن المؤقتة (توقع حركة مرور متكررة للمخازن المؤقتة ذات الحجم المتساوي تقريبًا) ، يمكنك استخدام نوع MemoryPool .
  • إذا كان يعني ضمنيًا أنك بحاجة إلى العمل باستخدام رمز unsafe ، أو تمرير مخزن مؤقت للبيانات هناك ، فيجب عليك استخدام نوع Memory : فهو يحتوي على طريقة Pin تعمل تلقائيًا على إصلاح المخزن المؤقت في كومة .NET إذا تم إنشاؤه هناك.
  • إذا كان لديك بعض حركة مرور المخزن المؤقت (على سبيل المثال ، يمكنك حل مشكلة تحليل نص البرنامج أو بعض DSL) ، فمن الجدير استخدام نوع MemoryPool ، والذي يمكن تنظيمه بطريقة صحيحة جدًا ، وإخراج المخازن المؤقتة بالحجم المناسب من التجمع (على سبيل المثال ، أكبر قليلاً إذا لم يكن مناسبًا ولكن مع التقليم originalMemory.Slice(requiredSize) حتى لا يتم تجزئة التجمع)

رابط للكتاب كله



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


All Articles