كل ما تريد معرفته عن جامع القمامة في بيثون

بشكل عام ، لا داعي للقلق بشأن جامع القمامة والعمل مع الذاكرة عند كتابة رمز Python. بمجرد عدم الحاجة إلى الكائنات ، تقوم Python تلقائيًا بتحرير الذاكرة من تحتها. على الرغم من ذلك ، سيساعدك فهم كيفية عمل GC على كتابة رمز أفضل.

مدير الذاكرة


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

بمجرد حذف أحد الكائنات الصغيرة - لا تنتقل الذاكرة من تحتها إلى نظام التشغيل ، تتركها Python لكائنات جديدة بنفس الحجم. إذا لم يكن هناك أي كائنات متبقية في أحد كتل الذاكرة المخصصة ، فيمكن لـ Python تحريرها إلى نظام التشغيل. عادة ، يتم تحرير الكتل عندما ينشئ البرنامج النصي العديد من الكائنات المؤقتة.

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

خوارزميات جمع القمامة


يستخدم مترجم بايثون القياسي (CPython) خوارزميتين في وقت واحد ، العد المرجعي ومجمع القمامة الأجيال (يشار إليه فيما يلي بـ GC) ، والمعروف باسم وحدة gc القياسية من Python.

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

في Python ، تعد خوارزمية الحساب المرجعي أساسية ولا يمكن تعطيلها ، في حين أن GC اختيارية ويمكن تعطيلها.

خوارزمية حساب الارتباط


خوارزمية حساب الارتباط هي واحدة من أسهل تقنيات جمع القمامة. يتم حذف الكائنات بمجرد عدم الرجوع إليها.

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

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

أمثلة عندما يزيد عدد الروابط:

  • عامل التعيين
  • تمرير الحجج
  • إدراج كائن جديد في الورقة (يزيد عدد الارتباطات للكائن)
  • بناء النموذج foo = bar (foo يبدأ بالإشارة إلى نفس الكائن مثل bar)

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

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

تسمى المتغيرات التي يتم الإعلان عنها خارج الدالات والفئات والكتل العمومية. عادة ، تكون دورة حياة هذه المتغيرات مساوية لحياة عملية Python. وبالتالي ، لا ينخفض ​​عدد الإشارات إلى الكائنات التي تشير إليها المتغيرات العامة إلى الصفر.

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

يمكنك دائمًا التحقق من عدد الروابط باستخدام وظيفة sys.getrefcount .

مثال لعداد الارتباط:

 foo = [] # 2 ,    foo    getrefcount print(sys.getrefcount(foo)) def bar(a): # 4  #  foo,   (a), getrefcount       print(sys.getrefcount(a)) bar(foo) # 2 ,      print(sys.getrefcount(foo)) 

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

الميزة الرئيسية لهذه الخوارزمية هي حذف الكائنات على الفور بمجرد عدم الحاجة إليها.

جامع القمامة الاختياري


لماذا نحتاج إلى خوارزمية إضافية عندما يكون لدينا بالفعل حساب مرجع؟

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

مثالان:

الصورة

كما ترى ، يشير الكائن lst إلى نفسه ، بينما يشير الكائن lst الكائن object2 إلى بعضهما البعض. بالنسبة إلى هذه الكائنات ، سيكون العدد المرجعي دائمًا 1.

عرض بايثون:

 import gc #  ctypes        class PyObject(ctypes.Structure): _fields_ = [("refcnt", ctypes.c_long)] gc.disable() #   GC lst = [] lst.append(lst) #    lst lst_address = id(lst) #   lst del lst object_1 = {} object_2 = {} object_1['obj2'] = object_2 object_2['obj1'] = object_1 obj_address = id(object_1) #   del object_1, object_2 #          # gc.collect() #    print(PyObject.from_address(obj_address).refcnt) print(PyObject.from_address(lst_address).refcnt) 

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

لإصلاح هذه المشكلة ، تمت إضافة خوارزمية إضافية تعرف باسم وحدة gc في Python 1.5. المهمة الوحيدة هي إزالة الكائنات الدائرية التي لم يعد من الممكن الوصول إليها من التعليمات البرمجية.

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

عندما يتم تشغيل GC


على عكس خوارزمية العد المرجعية ، لا يعمل GC الدوري في الوقت الفعلي ويتم تشغيله بشكل دوري. يؤدي كل تشغيل للجامع إلى توقف مؤقت في الشفرة ، لذلك يستخدم CPython (المترجم المعياري) استدلالات متنوعة لتحديد تكرار جامع القمامة.

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

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

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

يتم تعيين العتبات القياسية للأجيال على 700, 10 10 على التوالي ، ولكن يمكنك دائمًا تغييرها باستخدام gc.get_threshold gc.set_threshold .

حلقة خوارزمية البحث


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

لفهم أعمق ، أوصي بقراءة (ملاحظة المترجم: مادة اللغة الإنجليزية) الوصف الأصلي للخوارزمية من نيل شيميناور ووظيفة التجميع من مصادر CPython . يمكن أيضًا أن يكون وصفًا من Quora ومنشورًا حول جامع القمامة مفيدًا.

تجدر الإشارة إلى أن المشكلة مع المدمرات الموصوفة في الوصف الأصلي للخوارزمية قد تم إصلاحها منذ Python 3.4 (مزيد من التفاصيل في PEP 442 ).

نصائح للتحسين


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

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

في بعض الحالات ، من المفيد تعطيل الإنشاء التلقائي بواسطة وحدة gc واستدعائه يدويًا. للقيام بذلك ، ما gc.disable() سوى استدعاء gc.disable() ثم استدعاء gc.collect() يدويًا.

كيفية العثور على الروابط الدائرية وتصحيحها


يمكن أن تكون حلقات التصحيح مؤلمة ، خاصة إذا كانت الشفرة تستخدم العديد من وحدات الجهات الخارجية.

توفر وحدة gc وظائف مساعدة يمكنها المساعدة في تصحيح الأخطاء. إذا تم تعيين معلمات GC إلى علامة DEBUG_SAVEALL ، فستتم إضافة جميع الكائنات التي يتعذر الوصول إليها إلى قائمة gc.garbage .

 import gc gc.set_debug(gc.DEBUG_SAVEALL) print(gc.get_count()) lst = [] lst.append(lst) list_id = id(lst) del lst gc.collect() for item in gc.garbage: print(item) assert list_id == id(item) 

بمجرد تحديد نقطة المشكلة - يمكن تصورها باستخدام objgraph .

الصورة

الخلاصة

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

لا يجب أن تشارك في التحسين المبكر لأكواد جامع القمامة ؛ في الممارسة العملية ، مشاكل جمع القمامة نادرة جدًا.

ملاحظة: أنا مؤلف هذا المقال ، يمكنك طرح أي أسئلة.

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


All Articles