تحطيم أساسيات C #: تخصيص الذاكرة لنوع المرجع على المكدس

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



تنصل


لا تحتوي هذه المقالة على مواد يجب استخدامها في المشروعات الحقيقية. إنه ببساطة امتداد للحدود التي يتم فيها تصور لغة البرمجة.

قبل المتابعة مع القصة الإخبارية ، أوصيك بشدة أن تقرأ المنشور الأول حول 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..."); } } 


ودعنا نلقي نظرة على كيفية تقديمها في الذاكرة.
تعتبر هذه الفئة على سبيل المثال نظام 32 بت.



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

الحقل الأول (فهرس كتلة التزامن) لا يهمنا حقًا. عند وضع النوع قررت تخطيه. لقد فعلت ذلك لسببين:

  1. أنا كسول جدًا (لم أقل أن الأسباب معقولة)
  2. للتشغيل الأساسي للكائن ، هذا الحقل غير مطلوب.

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

الحقل الثاني هو أكثر أهمية بالنسبة لنا. بفضل جدول أساليب الكتابة ، من الممكن استخدام أداة قوية مثل تعدد الأشكال (والتي ، بالمناسبة ، الهياكل ، ملوك المكدس ، لا تمتلكها).
افترض أن فئة الموظف تنفذ ثلاث واجهات إضافية: IComparable و IDisposable و ICloneable.

ثم سيبدو جدول الطرق بشيء من هذا القبيل.



الصورة رائعة جدًا ، كل شيء معروض وكل شيء واضح. خلاصة القول ، الأسلوب الظاهري لا يسمى مباشرة بالعنوان ، ولكن بالإزاحة في جدول الطريقة. في التسلسل الهرمي ، سيتم وضع نفس الطرق الافتراضية في نفس الإزاحة في جدول الطريقة. وهذا هو ، في الفئة الأساسية نسميها بالطريقة "الإزاحة" ، دون معرفة نوع جدول الطريقة الذي سيتم استخدامه ، لكن مع العلم أن هذا الإزاحة سيكون الطريقة الأكثر ملاءمة لنوع وقت التشغيل.

كما تجدر الإشارة إلى أن مرجع الكائن يشير فقط إلى مؤشر جدول الطريقة.

المثال الذي طال انتظاره


لنبدأ بالفصول التي ستساعدنا في تحقيق هدفنا. باستخدام StructLayout (لقد جربت بالفعل بدون ذلك ، لكنه لم ينجح في ذلك) ، كتبت مصممي خرائط بسيطين - مؤشرات للأنواع المدارة والعودة. من السهل جدًا الحصول على مؤشر من رابط مدار ، لكن التحول العكسي سبب لي صعوبات ، وبدون التفكير مرتين ، قمت بتطبيق السمة المفضلة لدي. للحفاظ على الكود في مفتاح واحد ، صنع في اتجاهين بطريقة واحدة.

رمز المصممين
 // Provides the signatures we need public class PointerCasterFacade { public virtual unsafe T GetManagedReferenceByPointer<T>(int* pointer) => default(T); public virtual unsafe int* GetPointerByManagedReference<T>(T managedReference) => (int*)0; } // Provides the logic we need 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; } 



أولاً ، نكتب طريقة تأخذ مؤشرًا في بعض الذاكرة (وليس بالضرورة في المجموعة ، بالمناسبة) وتكوين الكتابة.

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

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

في الواقع ، هذا كل شيء. فجأة ، أليس كذلك؟ الآن لدينا نوع جاهز. سوف يتولى بينوكيو ، الذي خصص لنا الذاكرة ، تهيئة الحقول بنفسه.

يبقى فقط استخدام العجلات الضخمة الخاصة بنا لتحويل المؤشر إلى رابط مدار.
 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; } } 

الآن لدينا رابط على المكدس يشير إلى المكدس نفسه ، حيث يوجد وفقًا لجميع قوانين أنواع المراجع (جيدًا تقريبًا) كائن مبني من الأرض السوداء والعصي. تعدد الأشكال هو متاح.

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

نظرًا لأنه من المستحيل استخدام طريقة منفصلة للتهيئة على المكدس (نظرًا لأنه سيتم الكتابة فوق إطار المكدس بعد العودة من الطريقة) ، يجب أن تخصص الطريقة التي تريد تطبيق النوع على المكدس الذاكرة. بالمعنى الدقيق للكلمة ، هناك بعض الطرق للقيام بذلك. لكن الأنسب بالنسبة لنا هو stackalloc . مجرد كلمة رئيسية مثالية لأغراضنا. لسوء الحظ ، فإنه يجلب غير آمنة في التعليمات البرمجية. قبل ذلك ، كانت هناك فكرة لاستخدام 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

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


All Articles