Python menghabiskan banyak memori atau cara mengurangi ukuran objek?

Masalah memori dapat muncul ketika Anda perlu memiliki sejumlah besar objek selama eksekusi program, terutama jika ada batasan pada ukuran total RAM yang tersedia.


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


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


Diktik


Dalam program kecil, terutama 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 "kompak" di Python 3.6 dengan serangkaian kunci yang diperintahkan, dict menjadi lebih menarik. Namun, lihat ukuran jejaknya di RAM:


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

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


Jumlah salinanUkuran jejak
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 mendefinisikannya sebagai kelas dengan akses dengan 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:


LapanganUkuran (byte)
PyGC_Head24
PyObject_HEAD16
__weakref__8
__dict__8
TOTAL:56

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


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

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


Jumlah salinanUkuran jejak
1.000.000168 Mb
10.000.0001,68 GB
100.000.00016,8 GB

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


Instance kelas dengan __slots__


Pengurangan yang signifikan dalam jejak instance dalam memori dicapai dengan menghilangkan __dict__ dan __weakref__ . Ini dimungkinkan dengan "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 

Jejak dalam memori menjadi jauh lebih padat:


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

Menggunakan __slots__ dalam definisi kelas mengarah pada fakta bahwa jejak sejumlah besar contoh dalam memori berkurang secara signifikan:


Jumlah salinanUkuran jejak
1.000.00064 Mb
10.000.000640 Mb
100.000.0006,4 GB

Saat ini, ini adalah metode utama untuk secara signifikan mengurangi jejak instance kelas dalam memori program.


Pengurangan ini dicapai oleh fakta bahwa dalam memori setelah judul objek referensi ke objek disimpan, 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>}) 

Ada perpustakaan daftar nama untuk mengotomatiskan proses membuat kelas dengan __slots__ . Fungsi namedlist.namedlist membuat struktur kelas yang identik dengan kelas dengan __slots__ :


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

Paket attrs lain memungkinkan Anda untuk mengotomatiskan proses membuat kelas dengan dan tanpa __slots__ .


Tuple


Python juga memiliki tipe tuple untuk mewakili set data. Tuple adalah struktur atau catatan tetap, tetapi tanpa nama bidang. Untuk mengakses bidang, indeks bidang digunakan. Bidang tuple adalah sekali dan untuk semua yang terkait dengan objek nilai pada saat tuple dipakai:


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

Instance Tuple cukup kompak:


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

Mereka menempati 8 byte lebih banyak dalam memori daripada instance kelas dengan __slots__ , karena jejak tuple dalam memori juga berisi jumlah bidang:


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

Namedtuple


Karena tuple digunakan dengan sangat luas, suatu hari ada permintaan untuk dapat tetap memiliki akses ke bidang dengan nama juga. Respons terhadap permintaan ini adalah modul collections.namedtuple .


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


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

Itu menciptakan subkelas tuple, yang mendefinisikan pegangan 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_y(self): return self[2] # def __new__(cls, x, y, z): return tuple.__new__(cls, (x, y, z)) 

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


Jumlah salinanUkuran jejak
1.000.00072 Mb
10.000.000720 Mb

Recordclass: bermutasi nameduple tanpa GC


Sejak tuple dan, dengan demikian, kelas namedtuple menghasilkan objek yang tidak bisa berubah dalam arti bahwa objek nilai ob.x tidak lagi dapat dikaitkan dengan objek nilai lain, permintaan untuk varian namtuple yang termutasi telah muncul. Karena Python tidak memiliki tipe bawaan yang identik dengan tuple yang mendukung penugasan, banyak variasi telah dibuat. Kami akan fokus pada recordclass , yang menerima peringkat stackoverflow . Selain itu, dengan bantuannya dimungkinkan untuk mengurangi ukuran jejak suatu objek dalam memori dibandingkan dengan ukuran jejak objek jenis tuple .


Dalam paket recordclass , tipe recordclass.mutabletuple diperkenalkan, yang hampir identik dengan tuple tetapi juga mendukung penugasan. Pada dasarnya, subclass dibuat yang hampir identik dengan namedtuple, tetapi juga mendukung penugasan nilai baru ke bidang (tanpa membuat instance baru). Fungsi recordclass , seperti fungsi namedtuple , mengotomatiskan pembuatan kelas-kelas tersebut:


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

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


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

Secara default, fungsi recordclass memunculkan kelas yang tidak terlibat dalam mekanisme pengumpulan sampah melingkar. Biasanya, namedtuple dan recordclass digunakan untuk menelurkan kelas yang mewakili catatan atau struktur data sederhana (non-rekursif). Penggunaannya yang benar dalam Python tidak menghasilkan referensi melingkar. Untuk alasan ini, jejak instance kelas yang dihasilkan oleh PyGC_Head default PyGC_Head fragmen PyGC_Head , yang diperlukan untuk kelas yang mendukung mekanisme pengumpulan sampah siklik (lebih tepatnya: bendera PyTypeObject tidak disetel dalam bidang flags pada struktur PyTypeObject yang sesuai dengan kelas yang dibuat).


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


Jumlah salinanUkuran jejak
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: untuk menggunakan struktur penyimpanan dalam memori, seperti dalam contoh kelas dengan __slots__ , tetapi tidak untuk berpartisipasi dalam mekanisme pengumpulan sampah siklik. Kelas recordclass.make_dataclass menggunakan fungsi recordclass.make_dataclass :


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

Kelas default yang dibuat dengan cara ini membuat instance bermutasi.


Cara lain adalah dengan menggunakan deklarasi kelas dengan mewarisi dari recordclass.dataobject :


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

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


LapanganUkuran (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 relatif terhadap awal objek, yang ditempatkan 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>}) 

Ukuran jejak sejumlah besar contoh adalah yang sekecil mungkin untuk CPython:


Jumlah salinanUkuran jejak
1.000.00040 Mb
10.000.000400 Mb
100.000.0004,0 GB

Cython


Ada satu pendekatan yang didasarkan pada penggunaan Cython . Keuntungannya adalah bidang dapat mengambil nilai tipe 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 contoh dalam memori memiliki struktur berikut:


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

Ukuran jejak sejumlah besar salinan lebih kecil:


Jumlah salinanUkuran jejak
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 kali.


Numpy


Penggunaan 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 dan elemen N diinisialisasi dengan nol dibuat menggunakan fungsi:


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

Ukuran array adalah yang sekecil mungkin:


Jumlah salinanUkuran jejak
1.000.00012 Mb
10.000.000120 Mb
100.000.0001,20 GB

Akses reguler ke elemen dan string array akan memerlukan konversi objek Python
ke dalam nilai C int dan sebaliknya. Mengekstrak satu baris menghasilkan dalam array yang mengandung elemen tunggal. Jejaknya tidak akan begitu padat:


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


Dengan menggunakan contoh yang jelas dan sederhana, dimungkinkan untuk memverifikasi bahwa komunitas pengembang dan pengguna bahasa pemrograman Python (CPython) memiliki peluang nyata untuk secara signifikan mengurangi jumlah memori yang digunakan oleh objek.

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


All Articles