2.40. الذاكرة وجمع المهملات

يدير MicroPython الذاكرة بالطريقة نفسها التي تديرها بها CPython: كل كائن يعيش على الكومة (heap)، وجامع المهملات (GC) يحرر الكائنات التي لم يعد يشير إليها أي شيء. على جهاز ذي بضع مئات من الكيلوبايتات من ذاكرة RAM، يكون الانتباه إلى كيفية استخدام الكومة ضروريًا أحيانًا؛ وعلى Python سطح المكتب، نادرًا ما يكون كذلك.

2.40.1. الكومة

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

تستحق دالتان في الوحدة gc المعرفة:

  • gc.mem_free() -- العدد التقريبي للبايتات الحرة على الكومة في هذه اللحظة.

  • gc.collect() -- تشغيل دورة جمع فورًا بدلًا من انتظار زمن التشغيل ليطلق واحدة.

import gc

print("before:", gc.mem_free())
big = [0] * 10000
print("after :", gc.mem_free())
del big
gc.collect()
print("freed :", gc.mem_free())

الأرقام الدقيقة تعتمد على البناء؛ واتجاه التغيّر هو ما يهم: تخصيص أشياء كبيرة يقلل mem_free، وإسقاط المراجع مع gc.collect يعيد الكومة.

2.40.2. التجزئة

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

Two views of the heap. Before: one large contiguous free area. After: many small free areas interleaved with allocated blocks, none individually large enough for a big allocation.

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

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

2.40.3. التخصيص المسبق

نمطان يجعلان الكومة تتصرف على نحو حسن:

  • خصّص مخازن مؤقتة ثابتة الحجم مرة واحدة وأعد استخدامها، بدلًا من بناء قائمة جديدة أو bytearray في كل تكرار.

  • اسحب الثوابت وجداول البحث خارج الحلقات الداخلية بحيث تُنشأ مرة واحدة.

buf = bytearray(64)        # one allocation, reused below

def fill(value):
    for i in range(len(buf)):
        buf[i] = value

fill(0)
fill(255)

قارن بنسخة تنشئ bytearray جديدًا داخل الحلقة: كل تكرار ينتج مهملات يتعين على جامع المهملات تنظيفها لاحقًا. النسخة المخصّصة مسبقًا لا تنتج أي مهملات على الإطلاق.

2.40.4. متى تستدعي gc.collect

gc.collect() تكون عادةً تلقائية -- يطلقها زمن التشغيل عندما تعجز التخصيصات عن إيجاد ذاكرة حرة كافية. واستدعاؤها يدويًا مفيد في حالتين:

  • مباشرةً بعد خروج دفعة كبيرة من الكائنات من النطاق، لتحريرها فورًا بدلًا من انتظار التخصيص التالي ليدفع التكلفة.

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

نثر استدعاءات gc.collect في كل مكان لا يجعل البرنامج أسرع -- فالجمع نفسه يستغرق وقتًا. استخدمه عمدًا، عند نقاط تكون فيها تكلفة جمع غير مجدول أسوأ من تكلفة جمع شغّلته عن قصد.