Unsafe.AsSpan: Span كيفية استبدال المؤشرات؟


C# هي لغة مرنة بشكل لا يصدق. على ذلك ، يمكنك كتابة ليس فقط الخلفية أو تطبيقات سطح المكتب. يمكنني استخدام C# للعمل مع البيانات العلمية ، والتي تفرض متطلبات معينة على الأدوات المتاحة في اللغة. على الرغم من أن netcore يستحوذ على جدول الأعمال (بالنظر إلى أنه بعد netstandard2.0 ، لا يتم netstandard2.0 معظم ميزات كلتا اللغتين ووقت التشغيل في netframework ) ، إلا أنني أواصل العمل مع المشروعات القديمة.


في هذه المقالة ، أنظر إلى تطبيق واحد غير واضح (ولكن ربما يكون مرغوبًا فيه؟) لـ Span<T> والفرق بين تطبيق Span<T> في netframework و netcore بسبب تفاصيل clr .


إخلاء المسؤولية 1

مقتطفات الكود في هذه المقالة ليست مخصصة للاستخدام في مشاريع العالم الحقيقي.


الحل المقترح لمشكلة (بعيدة المنال؟) هو بالأحرى إثبات للمفهوم.
في أي حال ، من خلال تنفيذ هذا في مشروعك ، يمكنك القيام بذلك على مسؤوليتك الخاصة ومخاطرك.


إخلاء المسؤولية 2

أنا متأكد تمامًا أنه في مكان ما ، في بعض الحالات ، سيؤدي هذا بالتأكيد إلى إطلاق النار على شخص ما في الركبة.


من C# المرجح أن يؤدي تجاوز نوع الأمان في C# إلى أي شيء جيد.


لأسباب واضحة ، لم أختبر هذا الرمز في جميع الحالات الممكنة ، ومع ذلك ، فإن النتائج الأولية تبدو واعدة.


لماذا أحتاج Span<T> ؟


يتيح لك Spen التعامل مع صفائف من الأنواع unmanaged بشكل أكثر ملاءمة ، مما يقلل من عدد التخصيصات الضرورية. على الرغم من حقيقة أن الدعم netframework في netframework BCL غائب تمامًا تقريبًا ، يمكن الحصول على العديد من الأدوات باستخدام System.Memory و System.Buffers و System.Runtime.CompilerServices.Unsafe .
استخدام المسافات في مشروعي القديم محدود ، ومع ذلك ، فقد وجدت لهم استخدامًا غير واضح ، بينما بصق على أمان النوع.
ما هو هذا التطبيق؟ في مشروعي أعمل مع البيانات التي تم الحصول عليها من أداة علمية. هذه هي الصور ، والتي ، بشكل عام ، عبارة عن مجموعة من T[] ، حيث T هو أحد الأنواع البدائية unmanaged ، على سبيل المثال Int32 (المعروف أيضًا باسم int ). لتسلسل هذه الصور على القرص بشكل صحيح ، أحتاج إلى دعم التنسيق القديم غير المريح بشكل لا يصدق ، والذي تم اقتراحه في عام 1981 ، ومنذ ذلك الحين لم يتغير كثيرًا. المشكلة الرئيسية لهذا التنسيق هو BigEndian . وبالتالي ، من أجل كتابة (أو قراءة) مجموعة غير مضغوطة من T[] ، تحتاج إلى تغيير نهاية كل عنصر. المهمة التافهة.
ما هي بعض الحلول الواضحة؟


  1. نكررها عبر الصفيف T[] ، ندعو BitConverter.GetBytes(T) ، وسّع هذه البايتات القليلة ، وانسخها إلى الصفيف المستهدف.
  2. نكررها عبر الصفيف T[] ، وننفّذ عمليات احتيال للنموذج new byte[] {(byte)((x & 0xFF00) >> 8), (byte)(x & 0x00FF)}; (يجب أن تعمل على أنواع مزدوجة البايت) ، اكتب إلى الصفيف الهدف.
  3. * لكن هل T[] صفيف؟ العناصر في صف واحد ، أليس كذلك؟ حتى تتمكن من الانتقال ، على سبيل المثال ، Buffer.BlockCopy(intArray, 0, byteArray, 0, intArray.Length * sizeof(int)); . الأسلوب بنسخ الصفيف إلى الصفيف تجاهل تدقيق النوع. من الضروري فقط عدم تفويت الحدود والتخصيص. نحن مزيج البايتات نتيجة لذلك.
  4. * يقولون أن C# هو (C++)++ . لذلك ، قم بتمكين /unsafe ، fixed(int* p = &intArr[0]) byte* bPtr = (byte*)p; والآن يمكنك تشغيل تمثيل البايت للصفيف المصدر وتغيير النهاية على الطاير وكتابة الكتل على القرص (إضافة stackalloc byte[] أو ArrayPool<byte>.Shared المؤقت الوسيط) دون تخصيص ذاكرة لصفيف بايت جديد بالكامل.

يبدو أن النقطة 4 تسمح لك بحل جميع المشكلات ، لكن الاستخدام الصريح للسياق unsafe والعمل مع المؤشرات مختلف تمامًا. ثم Span<T> يأتي لمساعدتنا.


Span<T>


يجب أن يوفر Span<T> تقنيًا أدوات للعمل مع مؤامرات الذاكرة مثل العمل من خلال مؤشرات تقريبًا ، مع التخلص من الحاجة إلى "إصلاح" المصفوفة في الذاكرة. هذا مؤشر GC -aware مع حدود مجموعة. كل شيء على ما يرام وآمن.
شيء واحد ولكن - على الرغم من ثروة System.Runtime.CompilerServices.Unsafe ، Span<T> مسمر لكتابة T بالنظر إلى أن الدوران هو في الأساس مؤشر طول + 1 ، ماذا لو قمت بسحب المؤشر الخاص بك ، وقمت بتحويله إلى نوع آخر ، وإعادة حساب الطول وجعل فترة جديدة؟ لحسن الحظ ، لدينا public Span<T>(void* pointer, int length) .
دعنا نكتب اختبار بسيط:


 [Test] public void Test() { void Flip(Span<byte> span) {/*   endianess */} Span<int> x = new [] {123}; Span<byte> y = DangerousCast<int, byte>(x); Assert.AreEqual(123, x[0]); Flip(y); Assert.AreNotEqual(123, x[0]); Flip(y); Assert.AreEqual(123, x[0]); } 

المطورين أكثر تقدما مما يجب أن أدرك على الفور ما هو الخطأ هنا. هل سيفشل الاختبار؟ الجواب ، كما يحدث عادة ، يعتمد .
في هذه الحالة ، يعتمد بشكل أساسي على وقت التشغيل. على netcore يجب أن يعمل الاختبار ، ولكن على netframework ، كيف netframework .
ومن المثير للاهتمام ، إذا قمت بإزالة بعض المقالات ، يبدأ الاختبار في العمل بشكل صحيح في 100 ٪ من الحالات.
هيا بنا


1 كنت مخطئا .


الجواب الصحيح: يعتمد


لماذا تعتمد النتيجة؟
دعنا نزيل جميع الأشياء غير الضرورية والكتابة هنا مثل هذا الرمز:


 private static void Main() => Check(); private static void Check() { Span<int> x = new[] {999, 123, 11, -100}; Span<byte> y = As<int, byte>(ref x); Console.WriteLine(@"FRAMEWORK_NAME"); Write(ref x); Write(ref y); Console.WriteLine(); Write<int, int>(ref x, "Span<int> [0]"); Write<byte, int>(ref y, "Span<byte>[0]"); Console.WriteLine(); Write<int, int>(ref Offset<int, object>(ref x[0], 1), "Span<int> [0] offset by size_t"); Write<byte, int>(ref Offset<byte, object>(ref y[0], 1), "Span<byte>[0] offset by size_t"); Console.WriteLine(); GC.Collect(0, GCCollectionMode.Forced, true, true); Write<int, int>(ref x, "Span<int> [0] after GC"); Write<byte, int>(ref y, "Span<byte>[0] after GC"); Console.WriteLine(); Write(ref x); Write(ref y); } 

يقبل أسلوب Write<T, U> نطاقًا من النوع T ، ويقرأ عنوان العنصر الأول ، ويقرأ من خلال هذا المؤشر عنصرًا واحدًا من النوع U بمعنى آخر ، Write<int, int>(ref x) ستقوم بإخراج العنوان في الذاكرة + الرقم 999.
Write العادية يطبع مجموعة.
الآن حول الطريقة باسم As<,> :


  private static unsafe Span<U> As<T, U>(ref Span<T> span) where T : unmanaged where U : unmanaged { fixed(T* ptr = span) return new Span<U>(ptr, span.Length * Unsafe.SizeOf<T>() / Unsafe.SizeOf<U>()); } 

يدعم بناء جملة C# الآن سجل الحالة fixed عن طريق استدعاء أسلوب Span<T>.GetPinnableReference() .
قم بتشغيل هذه الطريقة على netframework4.8 في وضع x64 . نحن ننظر إلى ما يحدث:


 LEGACY [ 999, 123, 11, -100 ] [ 231, 3, 0, 0, 123, 0, 0, 0, 11, 0, 0, 0, 156, 255, 255, 255 ] 0x|00|00|02|8C|00|00|2F|B0 999 Span<int> [0] 0x|00|00|02|8C|00|00|2F|B0 999 Span<byte>[0] 0x|00|00|02|8C|00|00|2F|B8 11 Span<int> [0] offset by size_t 0x|00|00|02|8C|00|00|2F|B8 11 Span<byte>[0] offset by size_t 0x|00|00|02|8C|00|00|2B|18 999 Span<int> [0] after GC 0x|00|00|02|8C|00|00|2F|B0 6750318 Span<byte>[0] after GC [ 999, 123, 11, -100 ] [ 110, 0, 103, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] 

في البداية ، يتصرّف كلا Span<byte> (على الرغم من الأنواع المختلفة) بشكل متماثل ، ويمثل Span<byte> ، في جوهره ، طريقة عرض للصفيف الأصلي. ما تحتاجه.
حسنًا ، دعونا نحاول تحويل بداية الفترة إلى حجم IntPtr واحد (أو 2 X int على x64 ) والقراءة. نحصل على العنصر الثالث للصفيف والعنوان الصحيح. ثم سنقوم بجمع القمامة ...


 GC.Collect(0, GCCollectionMode.Forced, true, true); 

العلم الأخير في هذه الطريقة يطلب من GC ضغط الكومة. بعد استدعاء GC.Collect ينتقل GC الصفيف المحلي الأصلي. Span<int> تعكس هذه التغييرات ، لكن Span<byte> تستمر في الإشارة إلى العنوان القديم ، حيث من غير الواضح الآن ما. طريقة رائعة لاطلاق النار على نفسك كل ركبتيك دفعة واحدة!


الآن دعنا نلقي نظرة على نتيجة جزء الشفرة ذاته المحدد على netcore3.0.100-preview8 .


 CORE [ 999, 123, 11, -100 ] [ 231, 3, 0, 0, 123, 0, 0, 0, 11, 0, 0, 0, 156, 255, 255, 255 ] 0x|00|00|01|F2|8F|BD|C6|90 999 Span<int> [0] 0x|00|00|01|F2|8F|BD|C6|90 999 Span<byte>[0] 0x|00|00|01|F2|8F|BD|C6|98 11 Span<int> [0] offset by size_t 0x|00|00|01|F2|8F|BD|C6|98 11 Span<byte>[0] offset by size_t 0x|00|00|01|F2|8F|BD|BF|38 999 Span<int> [0] after GC 0x|00|00|01|F2|8F|BD|BF|38 999 Span<byte>[0] after GC [ 999, 123, 11, -100 ] [ 231, 3, 0, 0, 123, 0, 0, 0, 11, 0, 0, 0, 156, 255, 255, 255 ] 

كل شيء يعمل ، ويعمل بثبات ، بقدر ما أستطيع أن أرى. بعد الضغط ، يغيّر كلاهما مؤشرهما. ! ممتاز لكن كيف نجعلها تعمل في مشروع قديم؟


جيت جوهري


لقد نسيت تمامًا أن دعم netcore يتم تنفيذه في netcore خلال intrinsik . بمعنى آخر ، يمكن لـ netcore إنشاء مؤشرات داخلية حتى لجزء صفيف وتحديث الروابط بشكل صحيح عندما يتحرك GC . في netframework ، nuget تنفيذ nuget امتداد nuget . في الواقع ، لدينا نوعان مختلفان: واحد يتم إنشاؤه من الصفيف ويتتبع روابطه ، والثاني من المؤشر وليس لديه فكرة عما يشير إليه. بعد تحريك الصفيف الأصلي ، يستمر مؤشر span في الإشارة إلى المكان الذي تم تمرير المؤشر فيه في اتجاه مُنشئه. للمقارنة ، هذا مثال على تنفيذ span في netcore :


 readonly ref struct Span<T> where T : unmanaged { private readonly ByReference<T> _pointer; //  -   private readonly int _length; } 

وفي netframework :


 readonly ref struct Span<T> where T : unmanaged { private readonly Pinnable<T> _pinnable; private readonly IntPtr _byteOffset; private readonly int _length; } 

يحتوي _pinnable على إشارة إلى المصفوفة ، إذا تم تمريرها إلى المُنشئ ، فإن _byteOffset يحتوي على تحول (حتى في _byteOffset يحتوي على _byteOffset يحتوي على تحول غير صفري يتعلق بالطريقة التي يتم تمثيل المصفوفة في الذاكرة بها ، على الأرجح ). إذا قمت بتمرير مؤشر void* إلى المُنشئ ، _byteOffset تحويله ببساطة إلى _byteOffset مطلق. سيتم تسمير Span بإحكام في منطقة الذاكرة ، وستكون كل أساليب المثيل كثيرة بشروط مثل if(_pinnable is null) {/* */} else {/* _pinnable */} . ماذا تفعل في مثل هذه الحالة؟


كيفية القيام بذلك لا يستحق كل هذا العناء ، لكنني ما زلت كذلك


هذا القسم مخصص netframework المختلفة التي تدعمها netframework ، والتي تسمح بـ Span<T> -> Span<U> ، مع الاحتفاظ بجميع الروابط الضرورية.
أنا أحذرك: هذه منطقة برمجة غير طبيعية مع وجود أخطاء جوهرية وسلوك غير محدد في النهاية


الطريقة 1: ساذج


كما هو netframework في المثال ، لن يؤدي تحويل المؤشرات إلى إعطاء النتيجة المرجوة على netframework . نحن بحاجة إلى قيمة _pinnable . حسنًا ، سنكشف عن التفكير من خلال سحب الحقول الخاصة (سيئة للغاية وغير ممكنة دائمًا) ، وسوف نكتبها في نسج جديد ، وسنكون سعداء. هناك مشكلة صغيرة واحدة فقط: spen عبارة عن ref struct ، ولا يمكن أن تكون حجة عامة ، ولا يمكن تعبئتها في object . ستتطلب طرق الانعكاس القياسية ، بطريقة أو بأخرى ، دفع المدى إلى نوع المرجع. لم أجد طريقة بسيطة (حتى تفكر في التفكير في الحقول الخاصة).


الطريقة 2: نحن بحاجة للذهاب أعمق


كل شيء تم فعله بالفعل أمامي ( [1] ، [2] ، [3] ). Spen عبارة عن بنية ، بغض النظر عن T تشغل ثلاثة حقول نفس مقدار الذاكرة ( على نفس البنية ). ماذا لو [FieldOffset(0)] ؟ لم يقل قال من القيام به.


 [StructLayout(LayoutKind.Explicit)] ref struct Exchange<T, U> where T : unmanaged where U : unmanaged { [FieldOffset(0)] public Span<T> Span_1; [FieldOffset(0)] public Span<U> Span_2; } 

ولكن عند بدء تشغيل البرنامج (أو بالأحرى ، عند محاولة استخدام نوع) ، يجتمع TypeLoadException - لا يمكن أن يكون LayoutKind.Explicit . حسنًا ، لا يهم ، دعنا نسير على الطريق الصعب:


 [StructLayout(LayoutKind.Explicit)] public ref struct Exchange { [FieldOffset(0)] public Span<byte> ByteSpan; [FieldOffset(0)] public Span<sbyte> SByteSpan; [FieldOffset(0)] public Span<ushort> UShortSpan; [FieldOffset(0)] public Span<short> ShortSpan; [FieldOffset(0)] public Span<uint> UIntSpan; [FieldOffset(0)] public Span<int> IntSpan; [FieldOffset(0)] public Span<ulong> ULongSpan; [FieldOffset(0)] public Span<long> LongSpan; [FieldOffset(0)] public Span<float> FloatSpan; [FieldOffset(0)] public Span<double> DoubleSpan; [FieldOffset(0)] public Span<char> CharSpan; } 

الآن يمكنك القيام بذلك:


 private static Span<byte> As2(Span<int> span) { var exchange = new Exchange() { IntSpan = span }; return exchange.ByteSpan; } 

تعمل الطريقة مع مشكلة واحدة فقط - _length نسخ حقل _length كما هو ، لذلك عندما يكون _length int -> byte فإن مساحة البايت أصغر 4 مرات من المصفوفة الحقيقية.
ليست مشكلة:


 [StructLayout(LayoutKind.Sequential)] public ref struct Raw { public object Pinnable; public IntPtr Pointer; public int Length; } [StructLayout(LayoutKind.Explicit)] public ref struct Exchange { /* */ [FieldOffset(0)] public Raw RawView; } 

الآن من خلال RawView يمكنك الوصول إلى كل حقل RawView .


 private static Span<byte> As2(Span<int> span) { var exchange = new Exchange() { IntSpan = span }; var exchange2 = new Exchange() { RawView = new Raw() { Pinnable = exchange.RawView.Pinnable, Pointer = exchange.RawView.Pointer, Length = exchange.RawView.Length * sizeof<int> / sizeof<byte> } }; return exchange2.ByteSpan; } 

ويعمل كما ينبغي ، إذا تجاهلت استخدام الحيل القذرة. ناقص - لا يمكن إنشاء الإصدار العام للمحول ، يجب أن تكون راضيًا عن أنواع محددة مسبقًا.


الطريقة 3: مجنون


مثل أي مبرمج عادي ، أحب أتمتة الأشياء. الحاجة إلى كتابة المحولات لأي زوج من الأنواع unmanaged لم ترضاني. ما الحل الذي يمكن تقديمه؟ هذا صحيح ، احصل على CLR لكتابة رمز لك .


كيف تحقق هذا؟ هناك طرق مختلفة ، هناك مقالات . باختصار ، تبدو العملية كما يلي:
إنشاء مُنشئ بناء -> إنشاء مُنشئ وحدة -> إنشاء نوع -> {الحقول ، الطرق ، إلخ}} -> في الإخراج نحصل على مثيل لـ Type .
لفهم الشكل الذي يجب أن يبدو عليه النوع (إنه ref struct ildasm ) ، نستخدم أي أداة من نوع ildasm . في حالتي ، كان dotPeek .
يبدو إنشاء منشئ كتابة شيئًا مثل هذا:


 var typeBuilder = _mBuilder.DefineType($"Generated_{typeof(T).Name}", TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.ExplicitLayout // <-    | TypeAttributes.AnsiClass | TypeAttributes.BeforeFieldInit, typeof(ValueType)); 

الآن الحقول. نظرًا لأنه لا يمكننا نسخ Span<T> إلى Span<U> نظرًا للاختلاف في الأطوال ، نحتاج إلى إنشاء نوعين من كل فريق


 [StructLayout(LayoutKind.Explicit)] ref struct Generated_Int32 { [FieldOffset(0)] public Span<Int32> Span; [FieldOffset(0)] public Raw Raw; } 

هنا Raw يمكننا أن نعلن بأيدينا وإعادة استخدامها. لا تنسى عن IsByRefLikeAttribute . مع الحقول ، كل شيء بسيط:


 var spanField = typeBuilder.DefineField("Span", typeof(Span<T>), FieldAttributes.Private); spanField.SetOffset(0); var rawField = typeBuilder.DefineField("Raw", typeof(Raw), FieldAttributes.Private); rawField.SetOffset(0); 

هذا كل شيء ، أبسط نوع جاهز. الآن مخبأ وحدة التجميع. يتم تخزين الأنواع المخصصة مؤقتًا ، على سبيل المثال ، في القاموس ( T -> Generated_{nameof(T)} ). نقوم بإنشاء برنامج تغليف ، وفقًا TIn و TOut يولد نوعين من المساعدين ويقوم بتنفيذ العمليات اللازمة على النطاقات. هناك واحد ولكن. كما هو الحال في الانعكاس ، يكاد يكون من المستحيل استخدامه على مسافات (أو على ref struct أخرى). أو لم أجد حلاً بسيطًا . كيف تكون؟


مندوبي الانقاذ


طرق الانعكاس عادة ما تبدو مثل هذا:


  object Invoke(this MethodInfo mi, object @this, object[] otherArgs) 

إنهم لا يحملون معلومات حول الأنواع ، لذلك إذا كانت الملاكمة (= التغليف) مقبولة لك ، فلا توجد مشاكل.
في حالتنا ، يجب أن تحتوي @this و otherArgs على ref struct ، والتي لم أستطع الالتفاف عليها.
ومع ذلك ، هناك طريقة أكثر بساطة. دعنا نتخيل أن نوعًا ما يحتوي على أساليب getter و setter (وليس خصائص ، ولكنه أنشأ طرقًا بسيطة يدويًا).
على سبيل المثال:


 void Generated_Int32.SetSpan(Span<Int32> span) => this.Span = span; 

بالإضافة إلى الطريقة ، يمكننا إعلان نوع المفوض (بشكل صريح في الكود):


 delegate void SpanSetterDelegate<T>(Span<T> span) where T : unmanaged; 

يتعين علينا القيام بذلك لأن الإجراء القياسي يجب أن يكون له توقيع Action<Span<T>> ، ولكن لا يمكن استخدام spenes كوسيطات عامة. SpanSetterDelegate ، ومع ذلك ، هو مفوض صالحة تماما.
إنشاء المفوضين اللازمة. للقيام بذلك ، قم بإجراء عمليات معالجة قياسية:


 var mi = type.GetMethod("Method_Name"); // ,    public & instance var spanSetter = (SpanSetterDelegate<T>) mi.CreateDelegate(typeof(SpanSetterDelegate<T>), @this); 

الآن ، يمكن استخدام spanSetter(Span<T>.Empty); مثل ، spanSetter(Span<T>.Empty); . بالنسبة إلى @this 2 ، هذا هو مثيل @this الديناميكي ، الذي تم إنشاؤه ، بالطبع ، من خلال Activator.CreateInstance(type) ، لأن الهيكل له مُنشئ افتراضي بدون وسيطات.


لذا ، فإن الحدود الأخيرة - نحتاج إلى إنشاء طرق ديناميكية.


2 قد تلاحظ أن هناك شيئًا ما يحدث هنا - Activator.CreateInstance() بتعبئة مثيل ref struct . انظر نهاية القسم التالي.


تلبية Reflection.Emit


أعتقد أنه يمكن إنشاء طرق باستخدام Expression ، كما تتكون أجساد حرفنا / المستوطنين التافهين من بعض التعبيرات حرفيًا. اخترت نهجا مختلفا وأكثر مباشرة.


إذا netframework4.8 نظرة على رمز IL الخاص بجهاز تافه ، يمكنك رؤية شيء مثل ( Debug ، X86 ، netframework4.8 )


 nop ldarg.0 ldfld /* - */ stloc.0 br.s /*  */ ldloc.0 ret 

هناك الكثير من الأماكن للتوقف والتصحيح.
في إصدار الإصدار ، يبقى فقط الأكثر أهمية:


 ldarg.0 ldfld /* - */ ret 

الوسيطة فارغة أسلوب المثيل هي ... this . وبالتالي ، فإن ما يلي مكتوب باللغة IL :
1) قم بتنزيل this
2) تحميل قيمة الحقل
3) اعادته


هاه فقط؟ يحتوي Reflection.Emit على حمل زائد خاص يأخذ ، بالإضافة إلى كود المرجع ، أيضًا معلمة واصف الحقل. تمامًا كما تلقينا سابقًا ، على سبيل المثال spanField .


 var getSpan = type.DefineMethod("GetSpan", MethodAttributes.Public | MethodAttributes.HideBySig, CallingConventions.Standard, typeof(Span<T>), Array.Empty<Type>()); gen = getSpan.GetILGenerator(); gen.Emit(OpCodes.Ldarg_0); gen.Emit(OpCodes.Ldfld, spanField); gen.Emit(OpCodes.Ret); 

بالنسبة إلى المضبط ، الأمر أكثر تعقيدًا قليلاً ، تحتاج إلى تحميل هذا على المكدس ، وتحميل الوسيطة الأولى من الوظيفة ، ثم استدعاء تعليمات الكتابة في الحقل وإرجاع أي شيء:


 ldarg.0 ldarg.1 stfld /*   */ ret 

بعد القيام بهذا الإجراء بالنسبة للحقل Raw ، والإعلان عن المفوضين الضروريين (أو استخدام المفوضين القياسيين) ، نحصل على نوع ديناميكي وأربعة طرق وصول ، يتم من خلالها إنشاء المفوضين العامين الصحيحين.


نكتب فئة TIn ، باستخدام معلمتين TIn ( TIn ، TOut ) ، تستقبل مثيلات Type التي تشير إلى الأنواع الديناميكية (المخزنة مؤقتًا) المقابلة ، وبعد ذلك تقوم بإنشاء كائن واحد من كل نوع وتوليد أربعة مفوضين عامين ، وهما


  1. void SetSpan(Span<TIn> span) لكتابة المدى المصدر للهيكل
  2. Raw GetRaw() لقراءة محتويات مسافة كهيكل Raw
  3. void SetRaw(Raw raw) لكتابة بنية Raw المعدلة إلى الكائن الثاني
  4. Span<TOut> GetSpan() لإرجاع مدى النوع المرغوب به مع تعيين الحقول وإعادة حسابها بشكل صحيح.

ومن المثير للاهتمام ، يجب إنشاء مثيلات الكتابة الديناميكية مرة واحدة. عند إنشاء مفوض ، يتم تمرير مرجع إلى هذه الكائنات كمعلمة @this . هنا انتهاك للقواعد. Activator.CreateInstance إرجاع object . يبدو أن هذا يرجع إلى حقيقة أن النوع الديناميكي نفسه لم يتحول إلى ref فعل يشبه ref ( type.IsByRef Like == false ) ، ولكن كان من الممكن إنشاء حقول تشبه ref . على ما يبدو ، يوجد مثل هذا التقييد في اللغة ، ولكن CLR يهضمه. ربما هنا يتم إطلاق النار على الركبتين في حالة الاستخدام غير القياسي. 3


لذلك ، نحصل على مثيل من نوع عام يحتوي على أربعة مفوضين ومرجعين ضمنيين إلى مثيلات الفئات الديناميكية. يمكن إعادة استخدام المفوضين والهياكل عند إجراء نفس الطبقات في صف واحد. لتحسين الأداء ، نقوم بتخزين ذاكرة التخزين المؤقت مرة أخرى (بالفعل محول نوع) لزوج (TIn, TOut) -> Generator<TIn, TOut> .


السكتة الدماغية هي الأخيرة: نعطي أنواعًا ، Span<TIn> -> Span<TOut>


 public Span<TOut> Cast(Span<TIn> span) { //      if (span.IsEmpty) return Span<TOut>.Empty; // Caller   ,       if (span.Length * Unsafe.SizeOf<TIn>() % Unsafe.SizeOf<TOut>() != 0) throw new InvalidOperationException(); //      // Span<TIn> _input.Span = span; _spanSetter(span); //  Raw // Raw raw = _input.Raw; var raw = _rawGetter(); var newRaw = new Raw() { Pinnable = raw.Pinnable, //    Pinnable Pointer = raw.Pointer, //   Length = raw.Length * Unsafe.SizeOf<TIn>() / Unsafe.SizeOf<TOut>() //   }; //   Raw    // Raw _output.Raw = newRaw; _rawSetter(newRaw); //     // Span<TOut> _output.Span return _spanGetter(); } 

استنتاج


في بعض الأحيان - من أجل الاهتمام بالرياضة - يمكنك تجاوز بعض القيود المفروضة على اللغة وتنفيذ وظائف غير قياسية. بالطبع ، على مسؤوليتك الخاصة والمخاطر. تجدر الإشارة إلى أن الطريقة الديناميكية تسمح لك بالتخلي عن المؤشرات والسياقات unsafe / fixed ، والتي يمكن أن تكون مكافأة. الجانب السلبي الواضح هو الحاجة إلى التفكير وتوليد النوع.


بالنسبة لأولئك الذين قرأوا حتى النهاية.


نتائج المعيار الساذج

وما مدى سرعة كل شيء؟
قارنت سرعة الطوائف في سيناريو غبي لا يعكس الاستخدام الفعلي / المحتمل لهذه الطوائف والشقوق ، ولكن على الأقل يعطي فكرة عن السرعة.


  1. Cast_Explicitيستخدم التحويل من خلال نوع معلن صراحة ، كما في الطريقة الثانية . يتطلب كل طبقة تخصيص بنائين صغيرين والوصول إلى الحقول ؛
  2. Cast_ILتطبق الطريقة الثالثة ، ولكن في كل مرة يعيد فيها إنشاء مثيل Generator<TIn, TOut>، مما يؤدي إلى إجراء عمليات بحث مستمرة في القواميس ، بعد أن يولد التمريرة الأولى جميع الأنواع ؛
  3. Cast_IL_Cachedيتم تخزين نسخة المحول مباشرة Generator<TIn, TOut>، وهذا هو السبب في أنه يبدو أسرع في المتوسط ​​، لأن الطبقة الكاملة تتلخص في دعوات أربعة مندوبين ؛
  4. Buffer , , . .

int[N] N/2 .


, , . , . , , . , unmanaged .


 BenchmarkDotNet=v0.11.5, OS=Windows 10.0.18362 Intel Core i7-2700K CPU 3.50GHz (Sandy Bridge), 1 CPU, 8 logical and 4 physical cores [Host] : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.8.3815.0 Clr : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.8.3815.0 Job=Clr Runtime=Clr InvocationCount=1 UnrollFactor=1 

MethodNMeanخطأStdDevMedianRatioRatioSD
Cast_Explicit100362.2 ns18.0967 ns52.7888 ns400.0 ns1.000.00
Cast_IL1001,237.9 ns28.5954 ns67.4027 ns1,200.0 ns3.470.51
Cast_IL_Cached100522.8 ns25.2640 ns71.2576 ns500.0 ns1.460.27
Buffer100300.0 ns0.0000 ns0.0000 ns300.0 ns0.780.11
Cast_Explicit10002,628.6 ns54.0688 ns64.3650 ns2,600.0 ns1.000.00
Cast_IL10003,216.7 ns49.8568 ns38.9249 ns3,200.0 ns1.210.03
Cast_IL_Cached10002,484.6 ns44.9717 ns37.5534 ns2,500.0 ns0.940.02
Buffer10002,055.6 ns43.9695 ns73.4631 ns2,000.0 ns0.780.03
Cast_Explicit10000002,515,157.1 ns11,809.8538 ns10,469.1278 ns2,516,050.0 ns1.000.00
Cast_IL10000002,263,826.7 ns23,724.4930 ns22,191.9054 ns2,262,000.0 ns0.900.01
Cast_IL_Cached10000002,265,186.7 ns19,505.5913 ns18,245.5422 ns2,266,300.0 ns0.900.01
Buffer10000001,959,547.8 ns39,175.7435 ns49,544.7719 ns1,959,200.0 ns0.780.02
Cast_Explicit100000000255,751,392.9 ns2,595,107.7066 ns2,300,495.3873 ns255,298,950.0 ns1.000.00
Cast_IL100000000228,709,457.1 ns527,430.9293 ns467,553.7809 ns228,864,100.0 ns0.890.01
Cast_IL_Cached100000000227,966,553.8 ns355,027.3545 ns296,463.9203 ns227,903,600.0 ns0.890.01
Buffer100000000213,216,776.9 ns1,198,565.1142 ns1,000,856.1536 ns213,517,800.0 ns0.830.01

Acknowledgements

JetBrains ( :-)) R# VS standalone- dotPeek , . BenchmarkDotNet BenchmarkDotNet, youtube- NDC Conferences DotNext , , .


PS


3 , ref , , . ( ) . ref structs,


 static Raw Generated_Int32.GetRaw(Span<int> span) { var inst = new Generated_Int32() { Span = span }; return inst.Raw; } 

, Reflection.Emit . , ILGenerator.DeclareLocal .


 static Span<int> Generated_Int32.GetSpan(Raw raw); 


 delegate Raw GetRaw<T>(Span<T> span) where T : unmanaged; delegate Span<T> GetSpan<T>(Raw raw) where T : unmanaged; 

, , ref — . لأن ,


 var getter = type.GetMethod(@"GetRaw", BindingFlags.Static | BindingFlags.Public).CreateDelegate(typeof(GetRaw<T>), null) as GetRaw<T>; 


 Raw raw = getter(Span<TIn>.Empty); Raw newRaw = convert(raw); Span<TOut> = setter(newRaw); 

UPD01:

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


All Articles