Beberapa pemikiran tentang artikel ini.Baru-baru ini saya menjadi tertarik pada bagaimana Manajemen Memori Python bekerja di CPython untuk Python3
untuk 64-bit Ubuntu .
Sedikit teori
Pustaka sistem glibc memiliki pengalokasi malloc. Setiap proses memiliki area memori yang disebut heap. Dengan mengalokasikan memori secara dinamis dengan memanggil fungsi malloc, kita mendapatkan potongan dari tumpukan proses ini. Jika ukuran memori yang diminta kecil (tidak lebih dari 128KB), maka memori dapat diambil dari daftar potongan gratis. Jika ini tidak memungkinkan, maka memori akan
dialokasikan menggunakan panggilan sistem mmap
(sbrk, brk) . Panggilan sistem mmap memetakan memori virtual ke memori fisik. Memori ditampilkan dalam halaman 4KB. Potongan besar (lebih dari 128KB) selalu dialokasikan melalui panggilan sistem mmap. Saat membebaskan memori, jika sepotong kecil bebas berbatasan dengan area memori yang tidak dibekukan, maka sebagian memori dapat kembali ke sistem operasi. Potongan besar segera kembali ke sistem operasi.
Informasi diambil dari
kuliah tentang pengalokasi di C.CPython memiliki pengalokasi sendiri (PyMalloc) untuk "tumpukan pribadi" dan pengalokasi untuk setiap jenis objek yang bekerja "di atas" yang pertama. PyMalloc meminta potongan memori 256KB melalui malloc di pustaka sistem operasi yang disebut Arenas. Mereka, pada gilirannya, dibagi menjadi Pools dengan 4KB. Setiap Pool dibagi menjadi beberapa chunks dengan ukuran tetap, dan masing-masing dapat dibagi menjadi chunks dengan salah satu dari 64 ukuran.
Alokator untuk setiap jenis menggunakan Bongkahan yang sudah dialokasikan, jika ada. Jika tidak ada, PyMalloc akan mengeluarkan Pool baru dari Arena pertama, di mana ada tempat untuk Pool baru (Arena "diurutkan" dalam urutan hunian menurun). Jika ini tidak berhasil, maka PyMalloc meminta OS untuk Arena baru. Kecuali ketika ukuran memori yang diminta lebih dari 512B, maka memori tersebut dialokasikan langsung melalui malloc dari pustaka sistem.
Ketika suatu objek dihapus, memori tidak dikembalikan ke OS, tetapi Potongan hanya kembali ke Pools yang sesuai, dan Pools ke Arena. Arena kembali ke sistem operasi ketika semua bongkahan dibebaskan dari itu. Dari apa yang ternyata bahwa jika sejumlah kecil Chunks akan digunakan di Arena, maka semua memori yang sama di Arena akan digunakan oleh PVM. Tetapi karena potongan lebih dari 128KB dialokasikan melalui mmap, Arena bebas akan segera kembali ke sistem operasi.
Saya ingin fokus pada dua poin:
- Ternyata PyMalloc mengalokasikan 256KB memori fisik saat membuat Arena baru.
- Hanya Arena gratis yang dikembalikan ke sistem operasi.
Contoh
Perhatikan contoh berikut:
iterations = 2000000 l = [] for i in range(iterations): l.append(None) for i in range(iterations): l[i] = {} s = []
Dalam contoh, daftar l dari 2 juta elemen dibuat, yang semuanya mengarah ke satu objek Tidak ada. Dalam siklus berikutnya, sebuah objek dibuat untuk setiap elemen - kamus kosong. Kemudian daftar kedua dibuat, elemen yang menunjuk ke beberapa objek yang direferensikan oleh beberapa elemen dari daftar pertama. Setelah perayapan berikutnya, item dari daftar l lagi mulai menunjuk ke objek None. Dan pada siklus terakhir, kamus sekali lagi dibuat untuk setiap elemen dari daftar pertama.
Opsi daftar S:
s = []
s = l[::2]
s = l[200000 // 2::]
s = l[::100]
Kami tertarik pada konsumsi memori dalam setiap kasus.
Kami akan menjalankan skrip ini dengan mengaktifkan pencatatan PyMalloc:
export PYTHONMALLOCSTATS="True" && python3 source.py 2>result.txt
Penjelasan hasil
Dalam gambar Anda dapat melihat konsumsi memori di setiap kasus.
Pada sumbu absis tidak ada korelasi nilai dengan waktu ketika konsumsi tersebut terjadi, hanya setiap nilai dalam log dikaitkan dengan nomor seri.
"Tanpa elemen"
Dalam kasus pertama, daftar s kosong. Setelah membuat objek dalam siklus kedua, sekitar 500MB memori dikonsumsi. Dan semua objek ini dihapus pada siklus ketiga, dan memori yang digunakan dikembalikan ke sistem operasi. Pada siklus terakhir, memori untuk objek dialokasikan lagi, yang mengarah pada konsumsi 500MB yang sama.
"Setiap detik"
Dalam kasus ketika kita membuat daftar dengan setiap elemen kedua dari daftar l, kita dapat melihat bahwa memori tidak dikembalikan ke sistem operasi. Yaitu, dalam hal ini kami mengamati situasi di mana kamus sekitar 250MB dihapus, tetapi di setiap Kelompok ada Bongkahan yang tidak dihapus, karena itu Arena yang sesuai tidak dirilis. Tetapi, ketika kita membuat kamus untuk kedua kalinya, Potongan-potongan gratis dari Pools ini digunakan kembali, itulah sebabnya mengapa hanya sekitar 250MB memori baru dialokasikan.
"Babak kedua"
Dalam kasus ketika kita membuat daftar dari bagian kedua dari elemen daftar l, babak pertama adalah di Arena terpisah, karena yang sekitar 250MB memori dikembalikan ke sistem operasi. Setelah itu, sekitar 500MB dialokasikan kembali ke kamus baru, itulah sebabnya total konsumsi di wilayah 750MB.
Dalam kasus ini, tidak seperti yang kedua, memori sebagian dikembalikan ke sistem operasi. Yang, di satu sisi, memungkinkan proses lain untuk menggunakan memori ini, di sisi lain, membutuhkan panggilan sistem untuk membebaskan dan merealokasi itu.
"Setiap seratus"
Kasus terakhir tampaknya menjadi yang paling menarik. Di sana kami membuat daftar kedua dari setiap elemen keseratus dari daftar pertama, yang membutuhkan sekitar 5MB. Tetapi karena kenyataan bahwa sejumlah Chks yang diduduki tetap ada di masing-masing Arena, memori ini tidak dibebaskan, dan konsumsi tetap pada level 500MB. Saat kami membuat kamus untuk kedua kalinya, hampir tidak ada memori baru yang dialokasikan, dan potongan-potongan yang dialokasikan untuk pertama kali digunakan kembali.
Dalam situasi ini, karena fragmentasi memori, kami menggunakan 100 kali lebih banyak dari yang kami butuhkan. Tetapi, ketika memori ini diperlukan berulang kali, kita tidak perlu membuat panggilan sistem untuk mengalokasikannya.
Ringkasan
Perlu dicatat bahwa fragmentasi memori dimungkinkan ketika menggunakan banyak pengalokasi. Namun demikian, Anda perlu hati-hati menggunakan beberapa struktur data, misalnya, yang memiliki struktur pohon, seperti pohon pencarian. Karena operasi penambahan dan penghapusan yang sewenang-wenang dapat mengarah pada situasi yang dijelaskan di atas, karena itu kelayakan menggunakan struktur ini dalam hal konsumsi memori akan diragukan.
Kode untuk rendering gambar 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)