Python menghabiskan banyak memori atau cara mengurangi ukuran objek?

Masalah memori dapat muncul ketika sejumlah besar objek aktif dalam RAM selama pelaksanaan program, terutama jika ada batasan jumlah total memori yang tersedia.


Di bawah ini adalah ikhtisar dari beberapa metode untuk mengurangi ukuran objek, yang secara signifikan dapat mengurangi jumlah RAM yang diperlukan untuk program dalam Python murni.


Catatan: Ini adalah versi bahasa Inggris dari posting asli saya (dalam bahasa Rusia).


Untuk mempermudah, kami akan mempertimbangkan struktur dalam Python untuk mewakili titik dengan koordinat x , y , z dengan akses ke nilai koordinat berdasarkan nama.


Diktik


Dalam program kecil, terutama dalam skrip, cukup sederhana dan nyaman untuk menggunakan dict untuk mewakili informasi struktural:


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

Dengan munculnya implementasi yang lebih ringkas di Python 3.6 dengan serangkaian kunci yang tertata, dict telah menjadi lebih menarik. Namun, mari kita lihat ukuran jejaknya di RAM:


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

Dibutuhkan banyak memori, terutama jika Anda tiba-tiba harus membuat banyak contoh:


Jumlah contohUkuran benda
1.000.000240 Mb
10.000.0002,40 Gb
100.000.00024 gb

Contoh kelas


Bagi mereka yang suka berpakaian segala sesuatu di kelas, lebih baik untuk mendefinisikan struktur sebagai kelas dengan akses berdasarkan nama atribut:


 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 

Struktur instance kelas menarik:


BidangUkuran (byte)
PyGC_Head24
PyObject_HEAD16
__weakref__8
__dict__8
TOTAL:56

Di sini __weakref__ adalah referensi ke daftar yang disebut referensi lemah ke objek ini, bidang __dict__ adalah referensi ke kamus instance kelas, yang berisi nilai atribut instance (perhatikan bahwa platform referensi 64-bit menempati 8 byte). Mulai dengan Python 3.3, ruang bersama digunakan untuk menyimpan kunci dalam kamus untuk semua instance kelas. Ini mengurangi ukuran jejak instance dalam RAM:


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

Akibatnya, sejumlah besar instance kelas memiliki jejak yang lebih kecil dalam memori daripada kamus biasa ( dict ):


Jumlah contohUkuran
1.000.000168 Mb
10.000.0001,68 Gb
100.000.00016,8 Gb

Sangat mudah untuk melihat bahwa ukuran instance dalam RAM masih besar karena ukuran kamus instance.


Instance kelas dengan __slots__


Pengurangan yang signifikan dalam ukuran instance kelas dalam RAM dicapai dengan menghilangkan __dict__ dan __weakref__ . Ini dimungkinkan dengan bantuan "trik" dengan __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 

Ukuran objek dalam RAM menjadi lebih kecil secara signifikan:


BidangUkuran (byte)
PyGC_Head24
PyObject_HEAD16
x8
y8
z8
TOTAL:64

Menggunakan __slots__ dalam definisi kelas menyebabkan jejak sejumlah besar contoh dalam memori berkurang secara signifikan:


Jumlah contohUkuran
1.000.00064 Mb
10.000.000640 Mb
100.000.0006,4 Gb

Saat ini, ini adalah metode utama untuk secara substansial mengurangi jejak memori dari instance kelas di RAM.


Pengurangan ini dicapai oleh fakta bahwa dalam memori setelah judul objek, referensi objek disimpan - nilai atribut, dan akses ke mereka dilakukan menggunakan deskriptor khusus yang ada di kamus kelas:


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

Untuk mengotomatiskan proses membuat kelas dengan __slots__ , ada perpustakaan [daftar nama] ( https://pypi.org/project/namedlist ). Fungsi namedlist.namedlist membuat kelas dengan __slots__ :


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

Paket lain [attrs] ( https://pypi.org/project/attrs ) memungkinkan Anda untuk mengotomatiskan proses membuat kelas baik dengan dan tanpa __slots__ .


Tuple


Python juga memiliki tuple tipe tuple untuk mewakili struktur data yang tidak dapat diubah. Sebuah tuple adalah struktur atau catatan tetap, tetapi tanpa nama bidang. Untuk akses bidang, indeks bidang digunakan. Bidang tuple adalah sekali dan untuk semua yang terkait dengan objek nilai pada saat membuat instance tuple:


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

Contoh tuple cukup ringkas:


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

Mereka menempati 8 byte dalam memori lebih dari instance kelas dengan __slots__ , karena jejak tuple dalam memori juga berisi sejumlah bidang:


BidangUkuran (byte)
PyGC_Head24
PyObject_HEAD16
ob_size8
[0]8
[1]8
[2]8
TOTAL:72

Namedtuple


Karena tuple digunakan secara luas, suatu hari ada permintaan bahwa Anda masih dapat memiliki akses ke bidang dan nama juga. Jawaban atas permintaan ini adalah module collections.namedtuple .


Fungsi namedtuple dirancang untuk mengotomatiskan proses menghasilkan kelas-kelas tersebut:


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

Itu menciptakan subkelas tuple, di mana deskriptor didefinisikan untuk mengakses bidang dengan nama. Sebagai contoh kita, akan terlihat seperti ini:


  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)) 

Semua instance dari kelas tersebut memiliki jejak memori yang identik dengan tuple. Sejumlah besar contoh meninggalkan jejak memori yang sedikit lebih besar:


Jumlah contohUkuran
1.000.00072 Mb
10.000.000720 Mb
100.000.0007.2 Gb

Recordclass: dapat dinamai namesuple tanpa siklik GC


Karena tuple dan, dengan demikian, namedtuple -classes menghasilkan objek yang tidak dapat diubah dalam arti bahwa atribut ob.x tidak lagi dapat dikaitkan dengan objek nilai lain, permintaan untuk varian nametuple yang dapat berubah telah muncul. Karena tidak ada tipe bawaan pada Python yang identik dengan tuple yang mendukung penugasan, banyak opsi telah dibuat. Kami akan fokus pada [recordclass] ( https://pypi.org/project/recordclass ), yang menerima peringkat [stackoverflow] ( https://stackoverflow.com/questions/29290359/existence-of-mutable-named- tuple-in -python / 29419745). Selain itu dapat digunakan untuk mengurangi ukuran objek dalam RAM dibandingkan dengan ukuran objek seperti tuple ..


Paket recordclass memperkenalkan tipe recordclass.mutabletuple , yang hampir identik dengan tuple, tetapi juga mendukung penugasan. Pada dasarnya, subclass dibuat yang hampir sepenuhnya identik dengan namedtuple, tetapi juga mendukung penugasan nilai baru ke bidang (tanpa membuat instance baru). Fungsi recordclass , seperti fungsi namedtuple , memungkinkan Anda untuk mengotomatisasi pembuatan kelas-kelas ini:


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

Instance kelas memiliki struktur yang sama dengan tuple , tetapi hanya tanpa PyGC_Head :


BidangUkuran (byte)
PyObject_HEAD16
ob_size8
x8
y8
y8
TOTAL:48

Secara default, fungsi recordclass membuat kelas yang tidak berpartisipasi dalam mekanisme pengumpulan sampah siklik. Biasanya, namedtuple dan recordclass digunakan untuk menghasilkan kelas yang mewakili catatan atau struktur data sederhana (non-rekursif). Menggunakannya dengan benar di Python tidak menghasilkan referensi melingkar. Untuk alasan ini, setelah instance kelas yang dihasilkan oleh recordclass , secara 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 flag field, by default, the flag Py_TPFLAGS_HAVE_GC` tidak disetel).


Ukuran jejak memori dari sejumlah besar instance lebih kecil dari instance kelas dengan __slots__ :


Jumlah contohUkuran
1.000.00048 Mb
10.000.000480 Mb
100.000.0004,8 Gb

Objek data


Solusi lain yang diusulkan dalam perpustakaan recordclass didasarkan pada ide: gunakan struktur penyimpanan yang sama dalam memori seperti dalam instance kelas dengan __slots__ , tetapi jangan berpartisipasi dalam mekanisme pengumpulan sampah siklik. Kelas-kelas tersebut dihasilkan menggunakan fungsi recordclass.make_dataclass :


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

Kelas yang dibuat dengan cara ini, secara default, membuat instance yang bisa berubah.


Cara lain - gunakan deklarasi kelas dengan pewarisan dari recordclass.dataobject :


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

Kelas yang dibuat dengan cara ini akan membuat instance yang tidak berpartisipasi dalam mekanisme pengumpulan sampah siklik. Struktur instance dalam memori sama dengan dalam kasus dengan __slots__ , tetapi tanpa PyGC_Head :


BidangUkuran (byte)
PyObject_HEAD16
x8
y8
y8
TOTAL:40

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

Untuk mengakses bidang, deskriptor khusus juga digunakan untuk mengakses bidang dengan offsetnya dari awal objek, yang terletak di kamus kelas:


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

Sizeo dari jejak memori sejumlah besar contoh adalah minimum yang mungkin untuk CPython:


Jumlah contohUkuran
1.000.00040 Mb
10.000.000400 Mb
100.000.0004,0 Gb

Cython


Ada satu pendekatan berdasarkan penggunaan [Cython] ( https://cython.org ). Keuntungannya adalah bahwa bidang dapat mengambil nilai dari tipe atom bahasa C. Penjelas untuk mengakses bidang dari Python murni dibuat secara otomatis. Sebagai contoh:


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

Dalam hal ini, instans memiliki ukuran memori yang lebih kecil:


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

Jejak instance dalam memori memiliki struktur berikut:


BidangUkuran (byte)
PyObject_HEAD16
x4
y4
y4
kosong4
TOTAL:32

Ukuran jejak sejumlah besar salinan kurang:


NomorUkuran
1.000.00032 Mb
10.000.000320 Mb
100.000.0003,2 Gb

Namun, harus diingat bahwa ketika mengakses dari kode Python, konversi dari int ke objek Python dan sebaliknya akan dilakukan setiap waktu.


Numpy


Menggunakan array multidimensi atau array rekaman untuk sejumlah besar data memberikan keuntungan dalam memori. Namun, untuk pemrosesan efisien dalam Python murni, Anda harus menggunakan metode pemrosesan yang fokus pada penggunaan fungsi dari paket numpy .


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

Array elemen N , diinisialisasi dengan nol, dibuat menggunakan fungsi:


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

Ukuran array dalam memori seminimal mungkin:


Jumlah objekUkuran
1.000.00012 Mb
10.000.000120 Mb
100.000.0001,20 Gb

Akses normal ke elemen dan baris array akan memerlukan konversi dari objek Python ke nilai C int dan sebaliknya. Mengekstraksi satu baris menghasilkan penciptaan array yang mengandung elemen tunggal. Jejaknya tidak akan begitu kompak lagi:


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

Oleh karena itu, seperti disebutkan di atas, dalam kode Python, perlu untuk memproses array menggunakan fungsi dari paket numpy .


Kesimpulan


Pada contoh yang jelas dan sederhana, dimungkinkan untuk memverifikasi bahwa komunitas pengembang dan pengguna bahasa pemrograman Python (CPython) memiliki kemungkinan nyata untuk pengurangan signifikan dalam jumlah memori yang digunakan oleh objek.

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


All Articles