بايثون يستهلك الكثير من الذاكرة أو كيفية تقليل حجم الأشياء؟

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


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


ملاحظة: هذه هي النسخة الإنجليزية من منصبي الأصلي (باللغة الروسية).


للبساطة ، سننظر في الهياكل في بيثون لتمثيل النقاط مع الإحداثيات x ، y ، z مع الوصول إلى قيم الإحداثيات بالاسم.


ديكت


في البرامج الصغيرة ، وخاصة في البرامج النصية ، من السهل جدًا والمريح استخدام الأمر المضمن لتمثيل المعلومات الهيكلية:


 >>> ob = {'x':1, 'y':2, 'z':3} >>> x = ob['x'] >>> ob['y'] = y 

مع ظهور تطبيق أكثر إحكاما في Python 3.6 مع مجموعة من المفاتيح المطلوبة ، أصبح dict أكثر جاذبية. ومع ذلك ، دعونا نلقي نظرة على حجم بصمتها في ذاكرة الوصول العشوائي:


 >>> print(sys.getsizeof(ob)) 240 

يتطلب الأمر الكثير من الذاكرة ، خاصةً إذا كنت بحاجة فجأة إلى إنشاء عدد كبير من الحالات:


عدد الحالاتحجم الكائنات
1000000240 ميجا بايت
100000002.40 جيجا بايت
10000000024 غيغابايت

مثيل فئة


بالنسبة لأولئك الذين يحبون إمساك كل شيء في الفصول الدراسية ، من الأفضل تحديد الهياكل كفئة مع وصول باستخدام اسم السمة:


 class Point: # def __init__(self, x, y, z): self.x = x self.y = y self.z = z >>> ob = Point(1,2,3) >>> x = ob.x >>> ob.y = y 

هيكل مثيل الطبقة مثير للاهتمام:


حقلالحجم (بايت)
PyGC_Head24
PyObject_HEAD16
__weakref__8
__dict__8
المجموع:56

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


 >>> print(sys.getsizeof(ob), sys.getsizeof(ob.__dict__)) 56 112 

نتيجةً لذلك ، يكون لعدد كبير من مثيلات الفصل مساحة أصغر في الذاكرة من القاموس العادي ( dict ):


عدد الحالاتحجم
1000000168 ميجا بايت
100000001.68 جيجابايت
10000000016.8 جيجا بايت

من السهل أن نرى أن حجم المثيل في RAM لا يزال كبيرًا نظرًا لحجم قاموس المثيل.


نسخة من الفصل مع __slots__


يتم تحقيق انخفاض كبير في حجم مثيل فئة في ذاكرة الوصول العشوائي عن طريق التخلص من __dict__ و __weakref__ . هذا ممكن بمساعدة "خدعة" مع __slots__ :


 class Point: __slots__ = 'x', 'y', 'z' def __init__(self, x, y, z): self.x = x self.y = y self.z = z >>> ob = Point(1,2,3) >>> print(sys.getsizeof(ob)) 64 

أصبح حجم الكائن في RAM أصغر بكثير:


حقلالحجم (بايت)
PyGC_Head24
PyObject_HEAD16
س8
ذ8
ض8
المجموع:64

يؤدي استخدام __slots__ في تعريف الفئة إلى تقليل حجم عدد كبير من المثيلات في الذاكرة بشكل كبير:


عدد الحالاتحجم
100000064 ميغابايت
10000000640 ميجابايت
1000000006.4 جيجا بايت

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


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


 >>> pprint(Point.__dict__) mappingproxy( .................................... 'x': <member 'x' of 'Point' objects>, 'y': <member 'y' of 'Point' objects>, 'z': <member 'z' of 'Point' objects>}) 

لأتمتة عملية إنشاء فصل __slots__ باستخدام __slots__ ، هناك مكتبة [قائمة باسم] ( https://pypi.org/project/namedlist ). namedlist.namedlist الدالة namedlist.namedlist فئة مع __slots__ :


 >>> Point = namedlist('Point', ('x', 'y', 'z')) 

تسمح لك حزمة أخرى [attrs] ( https://pypi.org/project/attrs ) بأتمتة عملية إنشاء فئات مع __slots__ .


الصفوف (tuple)


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


 >>> ob = (1,2,3) >>> x = ob[0] >>> ob[1] = y # ERROR 

مثيلات tuple مضغوطة تمامًا:


 >>> print(sys.getsizeof(ob)) 72 

يشغلونها 8 بايت في الذاكرة أكثر من مثيلات الفئات التي تحتوي على __slots__ ، نظرًا لأن تتبع tuple في الذاكرة يحتوي أيضًا على عدد من الحقول:


حقلالحجم (بايت)
PyGC_Head24
PyObject_HEAD16
ob_size8
[0]8
[1]8
[2]8
المجموع:72

Namedtuple


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


تم namedtuple وظيفة namedtuple لأتمتة عملية إنشاء مثل هذه الفئات:


 >>> Point = namedtuple('Point', ('x', 'y', 'z')) 

يخلق فئة فرعية من tuple ، حيث يتم تعريف واصفات للوصول إلى الحقول بالاسم. على سبيل المثال ، لدينا يبدو مثل هذا:


  class Point(tuple): # @property def _get_x(self): return self[0] @property def _get_y(self): return self[1] @property def _get_z(self): return self[2] # def __new__(cls, x, y, z): return tuple.__new__(cls, (x, y, z)) 

جميع مثيلات هذه الفئات لها بصمة ذاكرة مماثلة لتلك الموجودة في tuple. يترك عدد كبير من الحالات بصمة ذاكرة أكبر قليلاً:


عدد الحالاتحجم
100000072 ميجا بايت
10000000720 ميجا بايت
1000000007.2 جيجا بايت

Recordclass: muttamedtuple دون دوري GC


نظرًا لأن tuple و ، بناءً على ذلك ، namedtuple كائنات ثابتة ، بمعنى أنه لم يعد بالإمكان ربط السمة ob.x بكائن قيمة آخر ، فقد نشأ طلب لمتغير اسمهtuple قابل للتغيير. نظرًا لعدم وجود أي نوع مضمن في Python مماثل للقيمة التي تدعم المهام ، فقد تم إنشاء العديد من الخيارات. سنركز على [recordclass] ( https://pypi.org/project/recordclass ) ، التي حصلت على تصنيف [stackoverflow] ( https://stackoverflow.com/questions/29290359/existence-of-mutable-named- tuple-in -python / 29419745). بالإضافة إلى أنه يمكن استخدامه لتقليل حجم الكائنات في ذاكرة الوصول العشوائي مقارنة بحجم الكائنات مثل tuple ..


تقدم الحزمة recordclass النوع recordclass.mutabletuple ، والذي يكاد يكون متطابقًا مع tuple ، ولكنه يدعم أيضًا الواجبات. على أساسها ، يتم إنشاء فئات فرعية تكون متطابقة تمامًا تقريبًا معtuples المسماة ، ولكنها تدعم أيضًا تعيين قيم جديدة للحقول (بدون إنشاء مثيلات جديدة). تتيح recordclass وظيفة recordclass ، مثل وظيفة namedtuple ، أتمتة إنشاء هذه الفئات:


  >>> Point = recordclass('Point', ('x', 'y', 'z')) >>> ob = Point(1, 2, 3) 

تحتوي مثيلات الفئة على نفس بنية tuple ، ولكن بدون PyGC_Head فقط:


حقلالحجم (بايت)
PyObject_HEAD16
ob_size8
س8
ذ8
ذ8
المجموع:48

بشكل افتراضي ، تقوم دالة recordclass بإنشاء فئة لا تشارك في آلية تجميع البيانات المهملة الدورية. عادةً ما يتم استخدام كل من recordclass و recordclass لإنشاء فئات تمثل السجلات أو هياكل بيانات بسيطة (غير متكررة). استخدامها بشكل صحيح في بيثون لا يولد مراجع دائرية. لهذا السبب ، في أعقاب مثيلات الفئات التي تم إنشاؤها بواسطة recordclass ، default, the fragment is excluded, which is necessary for classes supporting the cyclic garbage collection mechanism (more precisely: in the PyGC_Head fragment is excluded, which is necessary for classes supporting the cyclic garbage collection mechanism (more precisely: in the structure, corresponding to the created class, in the PyTypeObject structure, corresponding to the created class, in the field, by default, the flag الإشارات field, by default, the flag لم يتم تعيين field, by default, the flag Py_TPFLAGS_HAVE_GC`.


حجم مساحة الذاكرة لعدد كبير من المثيلات أصغر من مثيلات الفئة مع __slots__ :


عدد الحالاتحجم
100000048 ميجا بايت
10000000480 ميجا بايت
1000000004.8 جيجا بايت

Dataobject


يستند حل آخر مقترح في مكتبة recordclass إلى الفكرة: استخدم بنية التخزين نفسها في الذاكرة كما في مثيلات الفئة مع __slots__ ، لكن لا تشارك في آلية تجميع البيانات المهملة الدورية. يتم إنشاء هذه الفئات باستخدام دالة recordclass.make_dataclass :


  >>> Point = make_dataclass('Point', ('x', 'y', 'z')) 

الفئة التي تم إنشاؤها بهذه الطريقة ، بشكل افتراضي ، تنشئ مثيلات قابلة للتغيير.


طريقة أخرى - استخدام recordclass.dataobject الفئة مع الميراث من recordclass.dataobject :


 class Point(dataobject): x:int y:int z:int 

ستنشئ الفئات التي تم إنشاؤها بهذه الطريقة مثيلات لا تشارك في آلية تجميع البيانات المهملة الدورية. بنية المثيل في الذاكرة هي نفسها كما في الحالة مع __slots__ ، ولكن بدون PyGC_Head :


حقلالحجم (بايت)
PyObject_HEAD16
س8
ذ8
ذ8
المجموع:40

 >>> ob = Point(1,2,3) >>> print(sys.getsizeof(ob)) 40 

للوصول إلى الحقول ، يتم استخدام واصفات خاصة أيضًا للوصول إلى الحقل من خلال الإزاحة الخاصة به من بداية الكائن ، والتي توجد في قاموس الفئة:


 mappingproxy({'__new__': <staticmethod at 0x7f203c4e6be0>, ....................................... 'x': <recordclass.dataobject.dataslotgetset at 0x7f203c55c690>, 'y': <recordclass.dataobject.dataslotgetset at 0x7f203c55c670>, 'z': <recordclass.dataobject.dataslotgetset at 0x7f203c55c410>}) 

يعد حجم مساحة الذاكرة لعدد كبير من الحالات هو الحد الأدنى الممكن لـ CPython:


عدد الحالاتحجم
100000040 ميجا بايت
10000000400 ميجا بايت
1000000004.0 غيغابايت

Cython


هناك نهج واحد يعتمد على استخدام [Cython] ( https://cython.org ). ميزته هي أن الحقول يمكن أن تأخذ على قيم الأنواع الذرية للغة C. يتم إنشاء واصفات للوصول إلى الحقول من بيثون النقي تلقائيا. على سبيل المثال:


 cdef class Python: cdef public int x, y, z def __init__(self, x, y, z): self.x = x self.y = y self.z = z 

في هذه الحالة ، يكون للمثيلين حجم ذاكرة أصغر:


 >>> ob = Point(1,2,3) >>> print(sys.getsizeof(ob)) 32 

يحتوي تتبع مثيل في الذاكرة على البنية التالية:


حقلالحجم (بايت)
PyObject_HEAD16
س4
ذ4
ذ4
فارغ4
المجموع:32

حجم البصمة لعدد كبير من النسخ أقل:


عددحجم
100000032 ميجا بايت
10000000320 ميجا بايت
1000000003.2 جيجا بايت

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


نمباي


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


 >>> Point = numpy.dtype(('x', numpy.int32), ('y', numpy.int32), ('z', numpy.int32)]) 

يتم إنشاء مجموعة من العناصر N ، التي تمت تهيئتها باستخدام الأصفار ، باستخدام الوظيفة:


  >>> points = numpy.zeros(N, dtype=Point) 

حجم الصفيف في الذاكرة هو الحد الأدنى الممكن:


عدد الكائناتحجم
100000012 ميجا بايت
10000000120 ميجا بايت
1000000001.20 جيجا بايت

يتطلب الوصول العادي إلى عناصر الصفوف والصفوف التحويل من كائن Python إلى قيمة C int والعكس. يؤدي استخراج صف واحد إلى إنشاء صفيف يحتوي على عنصر واحد. لن يكون أثره مضغوطًا بعد الآن:


  >>> sys.getsizeof(points[0]) 68 

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


استنتاج


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

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


All Articles