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

تنويه
لا تحتوي هذه المقالة على المواد التي يجب استخدامها في المشاريع الحقيقية. إنها ببساطة امتداد للحدود التي تُدرك فيها لغة البرمجة.
قبل بدء القصة ، أوصيك بشدة بقراءة
المنشور الأول حول
StructLayout ، لأنه يتم تحليل مثال سيتم استخدامه في هذه المقالة (ومع ذلك ، كما هو الحال دائمًا).
الخلفية
بداية بكتابة كود هذا المقال ، أردت أن أفعل شيئًا مثيرًا للاهتمام باستخدام لغة التجميع. كنت أرغب في كسر نموذج التنفيذ القياسي بطريقة أو بأخرى والحصول على نتيجة غير عادية حقًا. وتذكرًا التكرار الذي يقول به الناس أن النوع المرجعي يختلف عن النوع المهم في أن الأول يقع على الكومة والثاني على المكدس ، قررت استخدام المجمع لإظهار أن النوع المرجعي يمكن أن يعيش على المكدس. ومع ذلك ، بدأت في مواجهة جميع أنواع المشاكل ، على سبيل المثال ، إعادة العنوان المطلوب وتمثيله كرابط مُدار (ما زلت أعمل عليه). لذلك بدأت بالخداع والقيام بما لا يعمل في المجمع ، في C #. وفي النهاية ، لم يبق المجمع على الإطلاق.
أيضًا ، توصية للقراءة - إذا كنت معتادًا على جهاز أنواع المراجع ، أوصي بتخطي النظرية المتعلقة بها (سيتم فقط إعطاء الأساسيات ، لا شيء مثير للاهتمام).
قليلا عن البنية الداخلية للأنواع
أود أن أذكر أن فصل الذاكرة على المكدس والكومة يحدث على مستوى .NET ، وهذا التقسيم منطقي بحت ، ولا يوجد فرق ماديًا بين مناطق الذاكرة تحت الكومة وتحت المكدس. يتم توفير الفرق في الإنتاجية بالفعل على وجه التحديد من خلال العمل مع هذه المجالات.
فكيف لتخصيص ذاكرة على المكدس؟ بادئ ذي بدء ، دعنا نرى كيف يتم بناء هذا النوع المرجعي الغامض وما هو موجود فيه ، وهو ليس ذو أهمية.
لذا ، فكر في أبسط مثال مع فئة الموظف.
كود الموظفpublic class Employee { private int _id; private string _name; public virtual void Work() { Console.WriteLine(“Zzzz...”); } public void TakeVacation(int days) { Console.WriteLine(“Zzzz...”); } public static void SetCompanyPolicy(CompanyPolicy policy) { Console.WriteLine("Zzzz..."); } }
وإلقاء نظرة على كيفية تقديمه في الذاكرة.
UPD: تعتبر هذه الفئة على سبيل المثال من نظام 32 بت.

وبالتالي ، بالإضافة إلى ذاكرة الحقول ، لدينا حقلين مخفيين آخرين - فهرس كتلة المزامنة (كلمة عنوان الكائن في الصورة) وعنوان جدول الطريقة.
الحقل الأول ، هو فهرس كتلة التزامن ، لسنا مهتمين بشكل خاص. عند وضع النوع ، قررت حذفه. فعلت هذا لسببين:
- أنا كسول جدا (لم أقل أن الأسباب ستكون معقولة)
- هذا الحقل اختياري للعمل الأساسي للكائن.
ولكن بما أننا تحدثنا بالفعل ، أعتقد أنه من الصحيح أن نقول بضع كلمات حول هذا المجال. يتم استخدامه لأغراض مختلفة (رمز التجزئة ، التزامن). بدلاً من ذلك ، فإن الحقل نفسه هو ببساطة فهرس لأحد كتل المزامنة المرتبطة بهذا الكائن. توجد الكتل نفسها في جدول كتل التزامن (صفيف عالمي). يعد إنشاء مثل هذه الكتلة عملية كبيرة إلى حد ما ، لذلك لا يتم إنشاؤها إذا لم تكن هناك حاجة إليها. علاوة على ذلك ، عند استخدام أقفال رقيقة ، سيتم كتابة معرف الخيط الذي تلقى القفل (بدلاً من الفهرس) هناك.
المجال الثاني أكثر أهمية بالنسبة لنا. بفضل جدول طرق الكتابة ، يمكن استخدام أداة قوية مثل تعدد الأشكال (والتي ، بالمناسبة ، لا يمتلكها الهيكل ، ملوك المكدس). افترض أن فئة الموظف تطبق ثلاث واجهات: ICparparable و IDisposable و ICloneable.
ثم سيبدو جدول الطريقة شيء مثل هذا

الصورة رائعة جدا ، من حيث المبدأ ، كل شيء مرسوم ومفهوم. إذا كانت قصيرة على الأصابع ، فعندئذٍ لا يتم استدعاء الطريقة الافتراضية مباشرة على العنوان ، ولكن عن طريق الإزاحة في جدول الطريقة. في التسلسل الهرمي ، سيتم وضع نفس الأساليب الافتراضية في نفس الإزاحة في جدول الطريقة. أي أننا نسمي الطريقة في الفئة الأساسية عند الإزاحة ، دون معرفة نوع جدول الطريقة الذي سيتم استخدامه ، ولكن مع العلم أنه عند هذا الإزاحة ستكون هناك الطريقة الأكثر صلة بنوع وقت التشغيل.
ومن الجدير بالذكر أيضًا أن الإشارة إلى الكائن تشير إلى جدول الطريقة.
المثال الذي طال انتظاره
لنبدأ بالفصول التي ستساعدنا في هدفنا. باستخدام StructLayout (حاولت حقًا بدونها ، لكنها لم تنجح) كتبت أبسط مصممي المؤشر للأنواع المُدارة والعكس صحيح. من السهل جدًا الحصول على مؤشر من رابط مُدار ، لكن التحول العكسي تسبب لي في صعوبات ، وبدون التفكير مرتين ، قمت بتطبيق السمة المفضلة. للحفاظ على الرمز في مفتاح واحد ، قمت بذلك في اتجاهين بطريقة واحدة.
الرمز هنا // public class PointerCasterFacade { public virtual unsafe T GetManagedReferenceByPointer<T>(int* pointer) => default(T); public virtual unsafe int* GetPointerByManagedReference<T>(T managedReference) => (int*)0; } // public class PointerCasterUnderground { public virtual T GetManagedReferenceByPointer<T>(T reference) => reference; public virtual unsafe int* GetPointerByManagedReference<T>(int* pointer) => pointer; } [StructLayout(LayoutKind.Explicit)] public class PointerCaster { public PointerCaster() { pointerCaster= new PointerCasterUnderground(); } [FieldOffset(0)] private PointerCasterUnderground pointerCaster; [FieldOffset(0)] public PointerCasterFacade Caster; }
أولاً ، اكتب طريقة تأخذ مؤشرًا إلى بعض الذاكرة (ليس بالضرورة على المكدس ، بالمناسبة) وتكوين النوع.
لسهولة العثور على عنوان جدول الطريقة ، أقوم بإنشاء نوع على كومة الذاكرة المؤقتة. أنا متأكد من أنه يمكن العثور على جدول الطريقة بطرق أخرى ، لكنني لم أحدد لنفسي هدف تحسين هذا الرمز ، كان الأمر أكثر إثارة للاهتمام بالنسبة لي لجعله مفهومًا. بعد ذلك ، باستخدام المحولات الموصوفة سابقًا ، نحصل على مؤشر للنوع الذي تم إنشاؤه.
يشير هذا المؤشر إلى جدول الطريقة بالضبط. لذلك ، يكفي فقط الحصول على المحتويات من الذاكرة التي تشير إليها. سيكون هذا عنوان جدول الطريقة.
وبما أن المؤشر الذي تم تمريره إلينا هو نوع من الإشارة إلى الكائن ، يجب علينا كتابة عنوان جدول الطريقة حيث يشير بالضبط.
هذا كل شيء في الواقع. بشكل غير متوقع ، أليس كذلك؟ الآن نوعنا جاهز. بينوكيو ، الذي خصص لنا الذاكرة ، سيهتم بتهيئة الحقول.
يبقى فقط لاستخدام grandcaster لتحويل المؤشر إلى ارتباط مُدار.
public class StackInitializer { public static unsafe T InitializeOnStack<T>(int* pointer) where T : new() { T r = new T(); var caster = new PointerCaster().Caster; int* ptr = caster.GetPointerByManagedReference(r); pointer[0] = ptr[0]; T reference = caster.GetManagedReferenceByPointer<T>(pointer); return reference; } }
الآن لدينا رابط على المكدس يشير إلى المكدس نفسه ، حيث تكمن جميع قوانين الأنواع المرجعية (جيدًا تقريبًا) في جسم بني من التربة السوداء والعصي. تعدد الأشكال متاح.
يجب أن يكون مفهوما أنه إذا قمت بتمرير هذا الرابط خارج الطريقة ، فبعد العودة منه ، سنحصل على شيء غير واضح. لا يمكن الحديث عن مكالمات إلى طرق افتراضية ؛ فلنطير بشكل استثنائي. يتم استدعاء الطرق العادية مباشرة ، في الكود سيكون هناك ببساطة عناوين للطرق الحقيقية ، لذلك ستعمل. وفي مكان الحقول ... ولكن لا أحد يعرف ماذا سيكون هناك.
نظرًا لأنه من المستحيل استخدام طريقة منفصلة للتهيئة على المكدس (حيث سيتم مسح إطار المكدس بعد العودة من الطريقة) ، يجب تخصيص الذاكرة بالطريقة التي تريد استخدام النوع على المكدس. بالمعنى الدقيق للكلمة ، لا توجد طريقة واحدة للقيام بذلك. لكن الأنسب بالنسبة لنا هو المكدس. فقط الكلمة الرئيسية المثالية لأغراضنا. لسوء الحظ ، كان هو الذي أدخل عدم إمكانية التحكم في التعليمات البرمجية. قبل ذلك ، كانت هناك فكرة لاستخدام Span لهذه الأغراض والاستغناء عن التعليمات البرمجية غير الآمنة. لا يوجد خطأ في التعليمات البرمجية غير الآمنة ، ولكن مثل أي مكان آخر ، فهي ليست رصاصة فضية ولها مجالات التطبيق الخاصة بها.
بعد ذلك ، بعد تلقي مؤشر للذاكرة على المكدس الحالي ، نقوم بتمرير هذا المؤشر إلى طريقة تشكل النوع في أجزاء. هذا هو كل من استمع - أحسنت.
unsafe class Program { public static void Main() { int* pointer = stackalloc int[2]; var a = StackInitializer.InitializeOnStack<StackReferenceType>(pointer); a.StubMethod(); Console.WriteLine(a.Field); Console.WriteLine(a); Console.Read(); } }
لا يجب استخدام هذا في المشاريع الحقيقية ، فالطريقة التي تخصص الذاكرة على المكدس تستخدم T () الجديد ، والذي بدوره يستخدم الانعكاس لإنشاء النوع على الكومة! لذلك ستكون هذه الطريقة أبطأ من الإنشاء المعتاد للنوع مرة واحدة ، حسنًا ، 40-50.
هنا يمكنك رؤية المشروع بأكمله.
المصدر: في الاستطراد النظري ، تم استخدام أمثلة من كتاب Sasha Goldstein - Pro .NET Performace