بيثون إدارة الذاكرة: قليلا عن تجزئة الذاكرة

بعض الأفكار حول هذا المقال .

لقد أصبحت مهتمًا مؤخرًا بكيفية عمل Python Memory Management في CPython لـ Python3 لـ Ubuntu 64 بت .

قليلا من الناحية النظرية


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

المعلومات مأخوذة من محاضرة عن الموزعين في C.

لدى CPython مُخصص خاص بها (PyMalloc) لـ "كومة الذاكرة المؤقتة الخاصة" ومخصصات لكل نوع من الكائنات التي تعمل "في المقدمة" من الأولى. تطلب PyMalloc قطع ذاكرة 256 كيلو بايت من خلال malloc في مكتبة نظام التشغيل تسمى Arenas. هم ، بدورهم ، ينقسمون إلى برك سباحة بواسطة 4 كيلوبايت. يتم تقسيم كل تجمع إلى قطع ذات حجم ثابت ، ويمكن تقسيم كل منها إلى قطع بحجم واحد من 64 مقاسًا.

يستخدم المخصصون لكل نوع القطع المخصصة بالفعل ، إن وجدت. في حالة عدم وجود أي منها ، ستصدر PyMalloc تجمع جديد من الساحة الأولى ، حيث يوجد مكان للسباحة الجديدة (يتم فرز "الساحات" بترتيب تنازلي لشغلها). إذا لم ينجح ذلك ، فسأطلب PyMalloc من نظام التشغيل الحصول على حلبة جديدة. باستثناء ما إذا كان حجم الذاكرة المطلوب أكثر من 512B ، يتم تخصيص الذاكرة مباشرة من خلال malloc من مكتبة النظام.

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

أود التركيز على نقطتين:

  1. اتضح أن PyMalloc يخصص 256 كيلو بايت من الذاكرة الفعلية عند إنشاء Arena جديد.
  2. يتم إرجاع Arenas المجاني فقط إلى نظام التشغيل.

مثال


النظر في المثال التالي:

iterations = 2000000 l = [] for i in range(iterations): l.append(None) for i in range(iterations): l[i] = {} s = [] # [1] # s = l[::2] # [2] # s = l[2000000 // 2::] # [3] # s = l[::100] # [4] for i in range(iterations): l[i] = None for i in range(iterations): l[i] = {} 

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

خيارات قائمة S:

  1.  s = [] 
  2.  s = l[::2] 
  3.  s = l[200000 // 2::] 
  4.  s = l[::100] 

نحن مهتمون باستهلاك الذاكرة في كل حالة.

سنقوم بتشغيل هذا البرنامج النصي مع تمكين تسجيل PyMalloc:

 export PYTHONMALLOCSTATS="True" && python3 source.py 2>result.txt 

شرح النتائج


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

صورة

"بدون عناصر"


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

"كل ثانية"


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

"النصف الثاني"


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

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

"كل مائة"


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

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

يؤدي


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

رمز لتقديم الصور
 def parse_result(filename): ms = [] with open(filename, "r") as f: for line in f: if line.startswith("Total"): m = float(line.split()[-1].replace(",", "")) / 1024 / 1024 ms.append(m) return ms ms_1 = parse_result("_1.txt") ms_2 = parse_result("_2.txt") ms_3 = parse_result("_3.txt") ms_4 = parse_result("_4.txt") import matplotlib.pyplot as plt plt.figure(figsize=(20, 15)) fontdict = { "fontsize": 20, "fontweight" : 1, } plt.subplot(2, 2, 1) plt.title(" ", fontdict=fontdict, loc="left") plt.plot(ms_1) plt.grid(b=True, which='major', color='#666666', linestyle='-.') plt.minorticks_on() plt.grid(b=True, which='minor', color='#999999', linestyle='-', alpha=0.2) plt.tick_params(axis='both', which='major', labelsize=15, labelbottom=False) plt.ylabel("MB", fontsize=15) plt.subplot(2, 2, 2) plt.title(" ", fontdict=fontdict, loc="left") plt.plot(ms_2) plt.grid(b=True, which='major', color='#666666', linestyle='-.') plt.minorticks_on() plt.grid(b=True, which='minor', color='#999999', linestyle='-', alpha=0.2) plt.tick_params(axis='both', which='major', labelsize=15, labelbottom=False) plt.ylabel("MB", fontsize=15) plt.subplot(2, 2, 3) plt.title(" ", fontdict=fontdict, loc="left") plt.plot(ms_3) plt.grid(b=True, which='major', color='#666666', linestyle='-.') plt.minorticks_on() plt.grid(b=True, which='minor', color='#999999', linestyle='-', alpha=0.2) plt.tick_params(axis='both', which='major', labelsize=15, labelbottom=False) plt.ylabel("MB", fontsize=15) plt.subplot(2, 2, 4) plt.title(" ", fontdict=fontdict, loc="left") plt.plot(ms_4) plt.grid(b=True, which='major', color='#666666', linestyle='-.') plt.minorticks_on() plt.grid(b=True, which='minor', color='#999999', linestyle='-', alpha=0.2) plt.tick_params(axis='both', which='major', labelsize=15, labelbottom=False) plt.ylabel("MB", fontsize=15) 

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


All Articles