
تقريبا جميع البرامج غير تافهة تخصيص واستخدام الذاكرة الديناميكية. إن القيام بذلك بشكل صحيح يزداد أهمية حيث تصبح البرامج أكثر تعقيدًا والأخطاء أكثر تكلفة.
المشاكل النموذجية هي:
- تسرب الذاكرة (عدم تحرير الذاكرة المستخدمة)
- إصدار مزدوج (إصدار الذاكرة أكثر من مرة)
- الاستخدام بعد الإصدار (استخدام مؤشر إلى ذاكرة تم تحريرها مسبقًا)
تتمثل المهمة في تتبع المؤشرات المسؤولة عن تحرير الذاكرة (أي أولئك الذين يمتلكون الذاكرة) ، وتمييز المؤشرات التي تشير ببساطة إلى جزء من الذاكرة ، والتحكم في موقعها ، وأي منها نشط (في النطاق).
الحلول النموذجية هي كما يلي:
- Garbage Collection (GC) - يمتلك GC كتل الذاكرة ويقوم بمسحها دوريًا بحثًا عن مؤشرات لهذه الكتل. إذا لم يتم العثور على مؤشرات ، يتم تحرير الذاكرة. هذا المخطط موثوق به ويستخدم في لغات مثل Go و Java. لكن GC تميل إلى استخدام ذاكرة أكثر من اللازم ، وتوقف مؤقتًا وتبطئ الكود بسبب إعادة التغليف (بوابات الكتابة الأصلية).
- مرجع العد (RC) - كائن RC يمتلك الذاكرة ويخزن عداد مؤشرات لنفسه. عندما تنخفض هذه العداد إلى الصفر ، يتم تحرير الذاكرة. إنها أيضًا آلية موثوقة ومقبولة بلغات مثل C ++ و ObjectiveC. RC هو ذاكرة فعالة ، بالإضافة إلى ذلك تتطلب مساحة فقط تحت العداد. الجوانب السلبية لـ RC هي النفقات العامة للحفاظ على العداد ، وتضمين معالج استثناء لضمان الحد منه ، والحظر الضروري للكائنات المشتركة بين تدفقات البرنامج. لتحسين الأداء ، يتم خداع المبرمجين أحيانًا عن طريق الإشارة مؤقتًا إلى كائن RC يتخطى العداد ، مما يؤدي إلى خطر القيام بذلك بشكل غير صحيح.
- التحكم اليدوي - إدارة الذاكرة اليدوية Sysalny malloc وخالية. إنها سريعة وفعالة فيما يتعلق باستخدام الذاكرة ، لكن اللغة لا تساعد على القيام بكل شيء بشكل صحيح ، معتمدة كلياً على تجربة وحماسة المبرمج. لقد كنت أستخدم malloc ومجانيًا لمدة 35 عامًا ، وبمساعدة تجربة مريرة لا تنتهي ، نادراً ما أرتكب أخطاء. لكن هذه ليست الطريقة التي يمكن أن تعتمد عليها تقنية البرمجة ، ولاحظ أن قلتي "نادراً" وليس "أبداً".
تعتمد الحلول 2 و 3 إلى درجة أو أخرى على الإيمان بالمبرمج للقيام بكل شيء بشكل صحيح. لا تعتمد الأنظمة القائمة على الإيمان جيدًا ، وقد ثبت أن إعادة فحص أخطاء إدارة الذاكرة أمر صعب للغاية (حيث أن بعض معايير الترميز تحظر استخدام الذاكرة الديناميكية).
ولكن هناك أيضًا طريقة رابعة - الملكية والاقتراض ، OB. إنه فعال في الذاكرة ، وبسرعة التشغيل اليدوي ، ويخضع لإعادة الفحص الآلي. تم نشر هذه الطريقة مؤخرًا من خلال لغة برمجة Rust. كما أن له عيوبه ، وخاصة الحاجة إلى إعادة التفكير في تخطيط الخوارزميات وهياكل البيانات.
يمكنك التعامل مع الجوانب السلبية ، والباقي من هذه المقالة هو وصف تخطيطي لكيفية عمل نظام OB وكيف نقترح كتابته باللغة D. لقد اعتبرت ذلك مستحيلًا في البداية ، لكنني بعد أن أمضيت بعض الوقت في التفكير ، وجدت طريقة. إنه مشابه لما قمنا به من خلال البرمجة الوظيفية - مع ثبات متعدية ووظائف "نقية".
حيازة
قرار من يملك الكائن في الذاكرة بسيط للغاية - هناك مؤشر واحد للكائن وهو المالك. وهو مسؤول أيضًا عن إطلاق الذاكرة ، وبعد ذلك يصبح غير صالح. نظرًا لحقيقة أن المؤشر إلى الكائن الموجود في الذاكرة هو المالك ، لا توجد مؤشرات أخرى داخل بنية البيانات هذه ، وبالتالي فإن بنية البيانات تشكل شجرة.
النتيجة الثانية هي أن المؤشرات تستخدم دلالات الحركة بدلاً من النسخ:
T* f(); void g(T*); T* p = f(); T* q = p;
يُحظر إزالة مؤشر من داخل بنية البيانات:
struct S { T* p; } S* f(); S* s = f(); T* q = sp;
لماذا لا تعتبر علامة sp غير صالحة؟ تكمن المشكلة في أن هذا سيتطلب إعداد التسمية في وقت التشغيل ، ولكن يجب حلها في مرحلة الترجمة ، لأنه ببساطة يعتبر خطأ في الترجمة.
خروج المؤشر الخاص خارج النطاق هو أيضًا خطأ:
void h() { T* p = f(); }
يجب تحريك قيمة المؤشر بشكل مختلف:
void g(T*); void h() { T* p = f(); g(p);
هذا يحل مشاكل تسرب الذاكرة والاستخدام بعد التحرير (تلميح: من أجل الوضوح ، استبدل f () بـ malloc () ، و g () بـ free ().)
يمكن التحقق من كل هذا في مرحلة التجميع باستخدام تقنية
تحليل تدفق البيانات (DFA) ، تمامًا مثل استخدامها
لإزالة تعبيرات فرعية شائعة ، ويمكن DFA الاسترخاء أي تشابك الفئران من انتقالات البرنامج التي قد تنشأ.
الاقتراض
نظام الحيازة الموضح أعلاه موثوق ، ولكنه مقيد للغاية.
النظر فيما يلي:
struct S { void car(); void bar(); } struct S* f(); S* s = f(); s.car();
لكي يعمل هذا ، يجب أن يكون لدى s.car () طريقة لاستعادة المؤشر عند الخروج.
هذه هي الطريقة التي يعمل الاقتراض. يأخذ s.car () نسخة من s لمدة s.car (). s غير صالح في وقت التشغيل ، ويصبح صالحًا مرة أخرى عند خروج s.car ().
في D ، تحصل دالات عضو
البنية على
هذا المؤشر حسب المرجع ، حتى نتمكن من تكييف الاستعارة بملحق صغير: الحصول على الوسيطة بالرجوع إليها.
يدعم D أيضًا نطاق المؤشرات ، لذا فإن الاقتراض أمر طبيعي:
void g(scope T*); T* f(); T* p = f(); g(p);
(عندما تتلقى الدالات الوسيطات حسب المرجع أو تستخدم المؤشرات ذات النطاق ، يُمنع تمديدها خارج حدود الوظيفة أو النطاق. وهذا يتوافق مع دلالات الاقتراض.)
الاقتراض بهذه الطريقة يضمن تفرد المؤشر لكائن في الذاكرة في أي لحظة معينة.
يمكن توسيع نطاق الاقتراض أكثر مع إدراك أن نظام الملكية يمكن الاعتماد عليه أيضًا ، حتى إذا تمت الإشارة إلى كائن من خلال عدة مؤشرات ثابتة (ولكن واحد فقط قابل للتغيير). لا يمكن لمؤشر ثابت تغيير الذاكرة أو تحريرها. هذا يعني أنه يمكن استعارة عدة مؤشرات ثابتة من المالك القابل للتغيير ، ولكن ليس له الحق في استخدامها أثناء وجود هذه المؤشرات الثابتة.
على سبيل المثال:
T* f(); void g(T*); T* p = f();
مبادئ
يمكن تقليص ما تقدم وفقًا للفهم التالي الذي يتصرف به كائن في الذاكرة كما لو كان في إحدى الحالتين:
- هناك بالضبط مؤشر واحد قابل للتغيير لها
- واحد أو أكثر من مؤشرات ثابتة إضافية
سوف يلاحظ القارئ اليقظ شيئًا غريبًا في ما كتبته: "كما لو". ماذا أريد أن ألمح إلى؟ ما الذي يحدث؟ نعم هناك واحد. تمتلئ لغات برمجة الكمبيوتر بكلمة "كما لو" تحت الغطاء ، شيء ما مثل الأموال الموجودة في حسابك المصرفي ليس موجودًا بالفعل (أعتذر إذا كانت هذه صدمة كبيرة لشخص ما) ، وهذا لا يختلف عن ذلك. اقرأ على!
لكن أولاً ، أعمق قليلاً في الموضوع.
دمج أساليب الملكية / الاقتراض في D
ألا تتعارض هذه التقنيات مع الطريقة التي يكتب بها الأشخاص عادةً في D ، ولن تنكسر جميع برامج D الحالية تقريبًا؟ وليس من السهل إصلاحه ، لكن إلى حد أنه يجب عليك إعادة تصميم جميع الخوارزميات من البداية؟
نعم فعلا ما لم يكن لدى D سلاح سري (تقريبًا): سمات الوظائف. اتضح أن دلالات الملكية / الاقتراض (OB) يمكن تنفيذها لكل وظيفة على حدة بعد التحليل الدلالي المعتاد. قد يلاحظ القارئ اليقظ أنه لم تتم إضافة أي بناء جملة جديد ، وقد تم فرض قيود فقط على التعليمات البرمجية الموجودة. يحتوي D بالفعل على سجل لاستخدام السمات الوظيفية لتغيير دلالاتها ، على سبيل المثال ، السمة
الخالصة لإنشاء وظائف "نقية". لتمكين دلالات OB ، تتم إضافة السمة @
live .
وهذا يعني أنه يمكن إضافة OB إلى رمز على D تدريجيا ، حسب الحاجة والموارد المجانية. هذا يجعل من الممكن إضافة OB ، وهذا أمر بالغ الأهمية ، ودعم المشروع باستمرار في حالة تعمل بشكل كامل ، واختبارها وجاهزة للإصدار. كما يسمح لك بأتمتة عملية مراقبة النسبة المئوية للمشروع التي تم نقلها بالفعل إلى OB. تتم إضافة هذه التقنية إلى قائمة ضمانات اللغة D الأخرى فيما يتعلق بموثوقية العمل مع الذاكرة (مثل التحكم في عدم توزيع المؤشرات إلى المتغيرات المؤقتة في الحزمة).
كما لو
لا يمكن تحقيق بعض الأشياء الضرورية من خلال الالتزام الصارم بـ OBs ، مثل كائنات حساب المرجع. بعد كل شيء ، تم تصميم كائنات RC لجعل العديد من المؤشرات لهم. نظرًا لأن كائنات RC آمنة عند العمل مع الذاكرة (إذا تم تنفيذها بشكل صحيح) ، فيمكن استخدامها مع OBs دون التأثير سلبًا على الموثوقية. لا يمكن إنشاؤها فقط باستخدام تقنية OB. الحل هو أن هناك سمات وظيفة أخرى في D ، مثل @
system . @
system هي ميزات يتم فيها تعطيل العديد من اختبارات الموثوقية. وبطبيعة الحال ، سيتم أيضًا تعطيل OB في الكود باستخدام
نظام @. هذا هو المكان الذي يختفي فيه تطبيق تقنية RC عن سيطرة OB.
لكن في الكود مع OB ، RC ، يبدو الكائن كما لو أنه يتبع جميع القواعد ، لذلك لا مشكلة!
سوف يستغرق الأمر عددًا من أنواع المكتبات المشابهة للعمل بنجاح مع OB.
استنتاج
هذه المقالة هي لمحة عامة أساسية عن التكنولوجيا OB. أنا أعمل على مواصفات أكثر تفصيلا. من الممكن أن أفتقد شيئًا ما وفي مكان ما حفرة أسفل الخط المائي ، لكن كل شيء يبدو جيدًا حتى الآن. هذا تطور مثير للغاية بالنسبة لـ D وأنا أتطلع إلى تنفيذه.
للحصول على مزيد من المناقشات والتعليقات من Walter ، يرجى الرجوع إلى الموضوعات على
/ r / program subreddit و
Hacker News .