
الذاكرة <T> و ReadOnlyMemory <T>
هناك اختلافات مرئية بين Memory<T>
و Span<T>
. الأول هو أن نوع Memory<T>
لا يحتوي على معدل ref
في رأس الكتابة. بمعنى آخر ، يمكن تخصيص نوع Memory<T>
على كل من المكدس بينما يكون إما متغيرًا محليًا أو معلمة أسلوب أو قيمته التي تم إرجاعها وعلى الكومة ، مع الإشارة إلى بعض البيانات الموجودة في الذاكرة من هناك. ومع ذلك ، فإن هذا الاختلاف الصغير يخلق تمييزًا كبيرًا في سلوك وقدرات Memory<T>
مقارنة بـ Span<T>
. بخلاف Span<T>
الذي يعد أداة لبعض الطرق لاستخدام بعض المخزن المؤقت للبيانات ، فإن نوع Memory<T>
مصمم لتخزين المعلومات حول المخزن المؤقت ، ولكن ليس لمعالجته. وبالتالي ، هناك اختلاف في API.
Memory<T>
على طرق للوصول إلى البيانات المسؤولة عنها. بدلاً من ذلك ، يحتوي على خاصية Span
وطريقة Slice
التي تُرجع مثيل نوع Span
.- بالإضافة إلى ذلك ، تحتوي
Memory<T>
على طريقة Pin()
المستخدمة في السيناريوهات عندما يجب تمرير بيانات المخزن المؤقت المخزنة إلى رمز unsafe
. إذا تم استدعاء هذه الطريقة عند تخصيص الذاكرة في .NET ، سيتم تثبيت المخزن المؤقت ولن يتحرك عندما يكون GC نشطًا. ستُرجع هذه الطريقة MemoryHandle
، والذي يُغلف GCHandle
للإشارة إلى جزء من العمر GCHandle
مخزن مؤقت للصفيف في الذاكرة.
تمت ترجمة هذا الفصل من اللغة الروسية بالاشتراك مع المؤلفين والمترجمين المحترفين . يمكنك مساعدتنا في الترجمة من الروسية أو الإنجليزية إلى أي لغة أخرى ، في المقام الأول إلى الصينية أو الألمانية.
وأيضًا ، إذا كنت تريد شكراً منا ، فإن أفضل طريقة للقيام بذلك هي منحنا نجمًا على github أو لتخزين المستودع
github / sidristij / dotnetbook .
ومع ذلك ، أقترح أن نتعرف على مجموعة كاملة من الفصول. أولاً ، دعنا ننظر إلى بنية 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); public Span<T> Span { get; } public unsafe MemoryHandle Pin(); }
كما نرى ، تحتوي البنية على المُنشئ استنادًا إلى المصفوفات ، لكنه يخزن البيانات في الكائن. هذا هو للإشارة إلى السلاسل التي لا تحتوي على مُنشئ مُصمم لها ، ولكن يمكن استخدامها مع طريقة string
AsMemory()
، فإنها تُرجع ReadOnlyMemory
. ومع ذلك ، حيث يجب أن يكون كلا النوعين متشابهين ، فإن Object
هو نوع حقل _object
.
بعد ذلك ، نرى اثنين من الصانعين على أساس MemoryManager
. سنتحدث عنها لاحقًا. خصائص الحصول على Length
(الحجم) و IsEmpty
تحقق من وجود مجموعة فارغة. أيضًا ، هناك طريقة Slice
للحصول على مجموعة فرعية وكذلك طرق CopyTo
و TryCopyTo
للنسخ.
الحديث عن Memory
أريد أن أصف طريقتين من هذا النوع بالتفصيل: خاصية Span
وطريقة Pin
.
الذاكرة <T>
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; } } }
وهي الخطوط التي تتعامل مع إدارة السلاسل. يقولون أننا إذا قمنا بتحويل ReadOnlyMemory<T>
إلى Memory<T>
(هذه الأشياء متشابهة في التمثيل الثنائي وهناك حتى تعليق يجب أن تتزامن هذه الأنواع بطريقة ثنائية حيث يتم إنتاج أحدها من آخر عن طريق استدعاء Unsafe.As
) سنحصل على ~ الوصول إلى غرفة سرية ~ مع فرصة لتغيير السلاسل. هذه آلية خطيرة للغاية:
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?
هذه الآلية جنبا إلى جنب مع التدرب على سلسلة يمكن أن تنتج عواقب وخيمة.
الذاكرة <T> .Pin
الطريقة الثانية التي تلفت الانتباه القوي هي 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; }
إنها أيضًا أداة مهمة للتوحيد لأنه إذا أردنا تمرير مخزن مؤقت إلى رمز غير مدار ، نحتاج فقط إلى استدعاء الأسلوب Pin()
وتمرير مؤشر إلى هذا الرمز بغض النظر عن نوع البيانات التي تشير إليها Memory<T>
. سيتم تخزين هذا المؤشر في خاصية الهيكل الناتج.
void PinSample(Memory<byte> memory) { using(var handle = memory.Pin()) { WinApi.SomeApiMethod(handle.Pointer); } }
لا يهم ما تم استدعاء Pin()
في هذا الرمز: يمكن أن يكون Memory
التي تمثل إما T[]
أو string
أو مخزن مؤقت للذاكرة غير المُدارة. ستحصل المصفوفات والسلسلة فقط على GCHandle.Alloc(array, GCHandleType.Pinned)
وفي حالة الذاكرة غير المدارة ، لن يحدث شيء.
MemoryManager ، IMemoryOwner ، MemoryPool
إلى جانب الإشارة إلى حقول البنية ، أريد أن أشير إلى أن هناك نوعان آخران من MemoryManager
كيان آخر - MemoryManager
. هذه ليست إدارة ذاكرة كلاسيكية ربما فكرت بها وسنتحدث عنها لاحقًا. مدير الذاكرة الكلاسيكية الذي ربما فكرت به وسنتحدث عنه لاحقًا. مثل Span
، تحتوي Memory
على مرجع إلى كائن تم التنقل وإزاحة وحجم مخزن مؤقت داخلي. لاحظ أنه يمكنك استخدام المشغل new
لإنشاء Memory
من صفيف فقط. أو يمكنك استخدام طرق التمديد لإنشاء Memory
من سلسلة أو صفيف أو 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); }
تشير هذه البنية إلى مالك نطاق الذاكرة. بمعنى آخر ، 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; } } } } } // Other methods }
هذا يعني أن الفصل يسمح بالمكالمات المتداخلة للأسلوب 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
مبنية حول نطاق ذاكرة غير مُدار ، فلن يتمكن Pin()
فعل أي شيء. ومع ذلك ، فإن هذا الزي يعمل مع أنواع مختلفة من المخازن المؤقتة: بالنسبة لكل من الشفرة المدارة وغير المدارة ، ستكون واجهة التفاعل هي نفسها. - كل نوع لديه المنشئات العامة. هذا يعني أنه يمكنك استخدام
Span
مباشرة أو الحصول على مثيله من Memory
. بالنسبة Memory
على هذا النحو ، يمكنك إنشائها بشكل فردي أو يمكنك إنشاء نطاق ذاكرة مملوك من قبل IMemoryOwner
إليه بواسطة Memory
. يمكن اعتبار أي نوع يعتمد على MemoryManger
كحالة محددة له بعض نطاق الذاكرة المحلية (على سبيل المثال ، مصحوبة بحساب المراجع من العالم unsafe
). بالإضافة إلى ذلك ، إذا كنت بحاجة إلى تجميع مثل هذه المخازن المؤقتة (من المتوقع أن تتكرر حركة المرور في المخازن المؤقتة بنفس الحجم تقريباً) ، يمكنك استخدام نوع MemoryPool
. - إذا كنت تنوي العمل باستخدام تعليمة برمجية
unsafe
بتمرير مخزن مؤقت للبيانات هناك ، فيجب عليك استخدام نوع Memory
الذي يحتوي على طريقة Pin()
التي تقوم تلقائيًا بربط مخزن مؤقت على كومة الذاكرة المؤقتة .NET إذا تم إنشاؤه هناك. - إذا كان لديك بعض حركة مرور المخازن المؤقتة (على سبيل المثال ، يمكنك تحليل نص برنامج أو DSL) ، فمن الأفضل استخدام نوع
MemoryPool
. يمكنك تنفيذه بشكل صحيح لإخراج المخازن المؤقتة ذات الحجم الضروري من تجمع (على سبيل المثال ، مخزن مؤقت أكبر قليلاً إذا لم يكن هناك واحد مناسب ، ولكن باستخدام originalMemory.Slice(requiredSize)
لتجنب تجزئة التجمع).
لقياس أداء أنواع البيانات الجديدة ، قررت استخدام مكتبة أصبحت بالفعل BenchmarkDotNet قياسية:
[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")); } }
الآن ، دعونا نرى النتائج.

بالنظر إليهم ، يمكننا الحصول على المعلومات التالية:
ArraySegment
فظيعة. ولكن إذا قمت بلفها في Span
يمكنك جعلها أقل فظاعة. في هذه الحالة ، سيزيد الأداء 7 مرات.- إذا أخذنا في الاعتبار .NET Framework 4.7.1 (نفس الشيء هو 4.5) ، فإن استخدام
Span
الأداء بشكل كبير عند العمل مع المخازن المؤقتة للبيانات. سوف تنخفض بنسبة 30-35 ٪. - ومع ذلك ، إذا نظرنا إلى .NET Core 2.1+ ، فإن الأداء سيظل متشابهًا أو حتى يزيد نظرًا لأن
Span
يمكنه استخدام جزء من مخزن البيانات المؤقت ، مما يؤدي إلى إنشاء السياق. يمكن العثور على الوظيفة نفسها في ArraySegment
، لكنها تعمل ببطء شديد.
وبالتالي ، يمكننا استخلاص استنتاجات بسيطة بشأن استخدام أنواع البيانات هذه:
- بالنسبة إلى
.NET Framework 4.5+
و .NET Core
لديهم الميزة الوحيدة: فهي أسرع من ArraySegment
عند التعامل مع مجموعة فرعية من صفيف أصلي ؛ - في
.NET Core 2.1+
ArraySegment
، يعطي استخدامهم ميزة لا يمكن إنكارها على كل من ArraySegment
وأي تطبيق يدوي لـ Slice
؛ - تعتبر الطرق الثلاث مثمرة قدر الإمكان ولا يمكن تحقيق ذلك باستخدام أي أداة لتوحيد الصفائف.
تمت ترجمة هذا الفصل من اللغة الروسية بالاشتراك مع المؤلفين والمترجمين المحترفين . يمكنك مساعدتنا في الترجمة من الروسية أو الإنجليزية إلى أي لغة أخرى ، في المقام الأول إلى الصينية أو الألمانية.
