حياة القيم وموتها
refcounting, generational GC, arena allocator — الذاكرة لا تعود للنظام
شغّل هذا الكود وراقب RSS (resident set size) باستخدام ps أو /proc/self/status:
def leak():
for _ in range(10**6):
x = [i for i in range(100)]
leak()
هل عادت الذاكرة للنظام؟ خمّن لماذا.
ثم:
import gc
class WithDel:
def __del__(self):
print("dying")
a = WithDel()
b = WithDel()
a.ref = b
b.ref = a
del a
del b
# لماذا لا يطبع "dying" هنا؟
gc.collect()
# والآن؟
اشرح لماذا يحتاج gc.collect() لإنهاء هذه الـ cycle.
إدارة الذاكرة التلقائية هي مقايضة. Reference counting بسيط وحتمي لكنه لا يكتشف cycles. GC يكتشف cycles لكنه غير حتمي ويعطل الأداء. بايثون تدمج الاثنين.
Reference Counting — كيف يعمل
كل PyObject يبدأ بـ ob_refcnt. Py_INCREF(op) يزيده، Py_DECREF(op) ينقصه. عندما يصل إلى 0، يُستدعى tp_dealloc الذي يحرّر الذاكرة. هذا فوري. لكن مشكلة cycles: A يشير لـ B و B يشير لـ A، refcount كل منهما = 1. لن يصل أبداً إلى 0.
Generational GC
افتح cpython/Modules/gcmodule.c. الـ GC يحتفظ بـ 3 generations (0, 1, 2). كل object يُنشأ يبدأ في generation 0. لو بقي حياً بعد فحص GC، يُنقل إلى generation 1، ثم 2. متى يعمل GC؟ بايثون تتتبّع عدد الـ allocations والـ deallocations. عندما allocations - deallocations > threshold لجيل معين، يشتغل GC لذلك الجيل. يستخدم mark-sweep: 1. Mark: ابدأ من roots (globals, stack frames, etc) وتتبّع كل reachable objects. 2. Sweep: كل object غير marked يُحرَّر.
__del__ والـ cycles — المشكلة
عندما يكتشف GC cycle من unreachable objects ويجد أن أحدها عنده __del__، لا يعرف كيف يقرر ترتيب الاستدعاء. القرار التصميمي: ضع هذه objects في gc.garbage ولا تحرّرها.
Allocator Hierarchy — لمَ لا تعود الذاكرة للنظام
بايثون لا تطلب الذاكرة من النظام لكل object. عندها هرم allocators: PyMem_RawAllocator → PyMem_Allocator → PyObject_Allocator → Arena Allocator (blocks of 256KB). الـ arena allocator يطلب 256KB من النظام. يوزّع منها blocks صغيرة لـ objects. عندما تُحرَّر objects، الـ blocks تُعاد إلى الـ arena، لكن الـ arena لا تُعاد للنظام. هذا يفسر: بايثون لا تعيد الذاكرة للنظام بعد تحرير objects.
Interning و Small Object Caches
بايثون تخزّن objects شائعة reuse: Small ints (-5 to 256) في longobject.c، String interning في unicodeobject.c، Tuple freelist (صغيرة لا تُحرّر)، List freelist، Async task freelist.
الممنوع: استخدام gc module أو weakref.
المطلوب: اكتب دالة تكشف المرجعية الدائرية (reference cycles) في أي object تُمرَّر إليها:
def find_cycles(root):
# تعيد list من cycles في الـ object graph المتصل بـ root
# كل cycle عبارة عن tuple من objects تشكل حلقة
# اطبع الـ id لكل object وعلاقاته
# استخدم gc.get_referents(obj) فقط
# نفّذ algorithm: تتبّع الـ object graph يدوياً واكتشف strongly connected components
pass
- الذاكرة في بايثون: reference counting (فوري) + generational GC (لـ cycles)
- الـ GC ثلاث generations: objects صغيرة تموت بسرعة، objects كبيرة تبقى
__del__+ cycle = خطر: objects تصبح uncollectable- Allocator hierarchy: arena > blocks > objects. الذاكرة لا تعود للنظام
- Interning و freelists: cached objects لتقليل الـ allocation overhead
Reference counting يعتمد على Py_INCREF / Py_DECREF لتعديل ob_refcnt. لكن ماذا لو كان threadان يعدّلان refcount لـ object واحد في نفس اللحظة؟ هذا سباق بيانات. حل C: الـ GIL يمنع التنفيذ المتزامن لـ bytecode — ولكن Py_INCREF و Py_DECREF نفسيهما ليسا atomic. كيف تحل C هذا؟ الجواب: الـ GIL يجعل race مستحيلاً — لأن التبديل بين threads يحدث فقط عند حدود bytecode instruction، لا في منتصف Py_INCREF.