Melepaskan memori dengan Python

128

Saya memiliki beberapa pertanyaan terkait penggunaan memori dalam contoh berikut.

  1. Jika saya menjalankan interpreter,

    foo = ['bar' for _ in xrange(10000000)]

    memori nyata yang digunakan pada mesin saya naik ke 80.9mb. Lalu saya,

    del foo

    memori nyata turun, tetapi hanya untuk 30.4mb. Penerjemah menggunakan 4.4mbbaseline jadi apa untungnya tidak melepaskan 26mbmemori ke OS? Apakah itu karena Python "merencanakan ke depan", berpikir bahwa Anda dapat menggunakan memori sebanyak itu lagi?

  2. Mengapa ia merilis 50.5mbsecara khusus - berapakah jumlah yang dilepaskan berdasarkan?

  3. Apakah ada cara untuk memaksa Python untuk melepaskan semua memori yang digunakan (jika Anda tahu Anda tidak akan menggunakan banyak memori lagi)?

CATATAN Pertanyaan ini berbeda dengan Bagaimana saya dapat secara eksplisit membebaskan memori dengan Python? karena pertanyaan ini terutama berkaitan dengan peningkatan penggunaan memori dari awal bahkan setelah penerjemah membebaskan benda melalui pengumpulan sampah (dengan menggunakan gc.collectatau tidak).

Jared
sumber
4
Perlu dicatat bahwa perilaku ini tidak spesifik untuk Python. Pada umumnya terjadi, ketika suatu proses membebaskan beberapa memori yang dialokasikan heap, memori tidak dilepaskan kembali ke OS sampai proses mati.
NPE
Pertanyaan Anda menanyakan banyak hal — beberapa di antaranya adalah dups, beberapa di antaranya tidak sesuai untuk SO, beberapa di antaranya mungkin merupakan pertanyaan yang bagus. Apakah Anda bertanya apakah Python tidak melepaskan memori, dalam keadaan apa tepatnya itu bisa / tidak bisa, apa mekanisme yang mendasarinya, mengapa ia dirancang seperti itu, apakah ada solusi, atau sesuatu yang lain sama sekali?
abarnert
2
@abarnert I menggabungkan subquestions yang serupa. Untuk menanggapi pertanyaan Anda: Saya tahu Python melepaskan beberapa memori ke OS tetapi mengapa tidak semuanya dan mengapa jumlah yang dilakukannya. Jika ada keadaan di mana tidak bisa, mengapa? Solusi apa juga.
Jared
@ jww saya tidak berpikir begitu. Pertanyaan ini benar-benar terkait dengan mengapa proses juru bahasa tidak pernah merilis memori bahkan setelah sepenuhnya mengumpulkan sampah dengan panggilan gc.collect.
Jared

Jawaban:

86

Memori yang dialokasikan pada heap dapat dikenakan tanda air tinggi. Ini rumit oleh optimasi internal Python untuk mengalokasikan objek kecil ( PyObject_Malloc) dalam 4 kolam KiB, dikelompokkan untuk ukuran alokasi pada kelipatan 8 byte - hingga 256 byte (512 byte dalam 3,3). Kolam itu sendiri berada di 256 arena KiB, jadi jika hanya satu blok dalam satu kolam digunakan, seluruh arena 256 KiB tidak akan dilepaskan. Dalam Python 3.3, pengalokasi objek kecil dialihkan untuk menggunakan peta memori anonim, bukan tumpukan, sehingga harus lebih baik dalam melepaskan memori.

Selain itu, tipe bawaan mempertahankan daftar bebas dari objek yang sebelumnya dialokasikan yang mungkin atau mungkin tidak menggunakan pengalokasi objek kecil. The inttipe mempertahankan daftar bebas dengan memori yang dialokasikan sendiri, dan kliring membutuhkan menelepon PyInt_ClearFreeList(). Ini bisa disebut secara tidak langsung dengan melakukan penuh gc.collect.

Cobalah seperti ini, dan beri tahu saya apa yang Anda dapatkan. Inilah tautan untuk psutil.Process.memory_info .

import os
import gc
import psutil

proc = psutil.Process(os.getpid())
gc.collect()
mem0 = proc.get_memory_info().rss

# create approx. 10**7 int objects and pointers
foo = ['abc' for x in range(10**7)]
mem1 = proc.get_memory_info().rss

# unreference, including x == 9999999
del foo, x
mem2 = proc.get_memory_info().rss

# collect() calls PyInt_ClearFreeList()
# or use ctypes: pythonapi.PyInt_ClearFreeList()
gc.collect()
mem3 = proc.get_memory_info().rss

pd = lambda x2, x1: 100.0 * (x2 - x1) / mem0
print "Allocation: %0.2f%%" % pd(mem1, mem0)
print "Unreference: %0.2f%%" % pd(mem2, mem1)
print "Collect: %0.2f%%" % pd(mem3, mem2)
print "Overall: %0.2f%%" % pd(mem3, mem0)

Keluaran:

Allocation: 3034.36%
Unreference: -752.39%
Collect: -2279.74%
Overall: 2.23%

Edit:

Saya beralih ke pengukuran relatif terhadap ukuran proses VM untuk menghilangkan efek dari proses lain dalam sistem.

C runtime (misalnya glibc, msvcrt) menyusut tumpukan ketika ruang kosong yang berdekatan di bagian atas mencapai ambang batas yang konstan, dinamis, atau dapat dikonfigurasi. Dengan glibc Anda dapat menyetel ini dengan mallopt(M_TRIM_THRESHOLD). Mengingat hal ini, tidak mengherankan jika tumpukan menyusut lebih - bahkan lebih banyak - daripada blok yang Anda free.

Dalam 3.x rangetidak membuat daftar, jadi tes di atas tidak akan membuat 10 juta intobjek. Bahkan jika itu terjadi, inttipe 3.x pada dasarnya adalah 2.x long, yang tidak menerapkan daftar freelist.

Eryk Sun
sumber
Gunakan memory_info()sebagai ganti get_memory_info()dan xdidefinisikan
Aziz Alto
Anda mendapatkan 10 ^ 7 intbahkan dalam Python 3, tetapi masing-masing menggantikan yang terakhir dalam variabel loop sehingga tidak semua ada sekaligus.
Davis Herring
Saya telah menemui masalah kebocoran memori, dan saya kira alasannya adalah apa yang Anda jawab di sini. Tapi bagaimana saya bisa membuktikan dugaan saya? Adakah alat yang bisa menunjukkan banyak pool yang di-mall-kan, tetapi hanya blok kecil yang digunakan?
ruiruige1991
130

Saya menduga pertanyaan yang sangat Anda pedulikan di sini adalah:

Apakah ada cara untuk memaksa Python untuk melepaskan semua memori yang digunakan (jika Anda tahu Anda tidak akan menggunakan banyak memori lagi)?

Tidak, tidak ada. Tetapi ada solusi yang mudah: proses anak.

Jika Anda membutuhkan penyimpanan sementara 500MB selama 5 menit, tetapi setelah itu Anda perlu menjalankan selama 2 jam lagi dan tidak akan menyentuh memori sebanyak itu lagi, menelurkan proses anak untuk melakukan pekerjaan intensif-memori. Ketika proses anak hilang, memori dilepaskan.

Ini tidak sepenuhnya sepele dan gratis, tetapi cukup mudah dan murah, yang biasanya cukup baik untuk perdagangan yang berharga.

Pertama, cara termudah untuk membuat proses anak adalah dengan concurrent.futures(atau, untuk 3.1 dan sebelumnya, futuresbackport pada PyPI):

with concurrent.futures.ProcessPoolExecutor(max_workers=1) as executor:
    result = executor.submit(func, *args, **kwargs).result()

Jika Anda perlu sedikit kontrol lagi, gunakan multiprocessingmodul.

Biaya adalah:

  • Proses startup agak lambat pada beberapa platform, terutama Windows. Kita berbicara milidetik di sini, bukan menit, dan jika Anda memutar satu anak untuk melakukan pekerjaan senilai 300 detik, Anda bahkan tidak akan menyadarinya. Tapi itu tidak gratis.
  • Jika jumlah besar memori sementara yang Anda gunakan benar-benar besar , melakukan hal ini dapat menyebabkan program utama Anda diganti. Tentu saja Anda menghemat waktu dalam jangka panjang, karena jika ingatan itu bertahan selamanya harus mengarah pada pertukaran di beberapa titik. Tapi ini bisa mengubah kelambatan bertahap menjadi penundaan yang sangat nyata (dan awal) sekaligus dalam beberapa kasus penggunaan.
  • Mengirim sejumlah besar data antar proses bisa lambat. Sekali lagi, jika Anda berbicara tentang mengirim lebih dari 2K argumen dan mendapatkan kembali 64K hasil, Anda bahkan tidak akan menyadarinya, tetapi jika Anda mengirim dan menerima data dalam jumlah besar, Anda akan ingin menggunakan mekanisme lain (file, mmapped atau sebaliknya; API memori bersama di multiprocessing; dll.)
  • Mengirim sejumlah besar data di antara proses berarti data harus dapat dipilih (atau, jika Anda menempelkannya dalam file atau memori bersama, struct-able atau idealnya ctypes-able).
abarnert
sumber
Trik yang sangat bagus, meskipun tidak menyelesaikan masalah :( Tapi saya sangat menyukainya
ddofborg
32

eryksun telah menjawab pertanyaan # 1, dan saya telah menjawab pertanyaan # 3 (yang asli # 4), tetapi sekarang mari kita jawab pertanyaan # 2:

Mengapa ia merilis 50,5mb khususnya - berapakah jumlah yang dilepaskan berdasarkan?

Apa yang menjadi mallocdasarnya adalah, pada akhirnya, seluruh rangkaian kebetulan di dalam Python dan itu sangat sulit diprediksi.

Pertama, tergantung pada bagaimana Anda mengukur memori, Anda mungkin hanya mengukur halaman yang sebenarnya dipetakan ke dalam memori. Dalam hal ini, setiap kali halaman ditukar oleh pager, memori akan muncul sebagai "dibebaskan", meskipun belum dibebaskan.

Atau Anda mungkin mengukur halaman yang sedang digunakan, yang mungkin atau mungkin tidak menghitung halaman yang dialokasikan tetapi tidak pernah disentuh (pada sistem yang optimis alokasi berlebihan, seperti linux), halaman yang dialokasikan tetapi ditandai MADV_FREE, dll.

Jika Anda benar-benar mengukur halaman yang dialokasikan (yang sebenarnya bukan hal yang sangat berguna untuk dilakukan, tetapi tampaknya itu yang Anda tanyakan), dan halaman benar-benar telah dialokasikan, dua keadaan di mana hal ini dapat terjadi: Entah Anda telah digunakan brkatau setara untuk mengecilkan segmen data (sangat jarang saat ini), atau Anda telah menggunakan munmapatau mirip untuk melepaskan segmen yang dipetakan. (Secara teoritis juga ada varian kecil untuk yang terakhir, dalam hal itu ada cara untuk melepaskan bagian dari segmen yang dipetakan — misalnya, mencurinya dengan MAP_FIXEDuntuk MADV_FREEsegmen yang Anda segera hapus peta.)

Tetapi sebagian besar program tidak secara langsung mengalokasikan hal-hal dari halaman memori; mereka menggunakan mallocpengalokasi-gaya. Saat Anda menelepon free, pengalokasi hanya dapat merilis halaman ke OS jika Anda kebetulan menjadi freeobjek langsung terakhir dalam pemetaan (atau di halaman N terakhir dari segmen data). Tidak mungkin aplikasi Anda dapat memprediksi hal ini secara wajar, atau bahkan mendeteksi bahwa itu terjadi sebelumnya.

CPython membuat ini lebih rumit - ia memiliki pengalokasi objek 2-tingkat kustom di atas pengalokasi memori kustom di atas malloc. (Lihat komentar sumber untuk penjelasan yang lebih terperinci.) Dan di atas itu, bahkan pada level C API, apalagi Python, Anda bahkan tidak secara langsung mengontrol ketika objek level atas dideallocated.

Jadi, ketika Anda merilis objek, bagaimana Anda tahu apakah itu akan melepaskan memori ke OS? Yah, pertama-tama Anda harus tahu bahwa Anda telah merilis referensi terakhir (termasuk referensi internal apa pun yang Anda tidak tahu), memungkinkan GC untuk membatalkan alokasi itu. (Tidak seperti implementasi lainnya, setidaknya CPython akan membatalkan alokasi objek segera setelah diizinkan.) Ini biasanya membatalkan alokasi setidaknya dua hal di tingkat berikutnya ke bawah (misalnya, untuk string, Anda melepaskan PyStringobjek, dan buffer string ).

Jika Anda melakukan deallocate objek, untuk mengetahui apakah ini menyebabkan tingkat berikutnya turun untuk mendelallocasi blok penyimpanan objek, Anda harus mengetahui keadaan internal pengalokasi objek, serta bagaimana itu diterapkan. (Ini jelas tidak dapat terjadi kecuali Anda membatalkan alokasi hal terakhir di blok, dan bahkan kemudian, itu mungkin tidak terjadi.)

Jika Anda melakukan deallocate blok penyimpanan objek, untuk mengetahui apakah ini menyebabkan freepanggilan, Anda harus mengetahui keadaan internal pengalokasi PyMem, serta bagaimana itu diterapkan. (Sekali lagi, Anda harus deallocating blok yang terakhir digunakan dalam mallocwilayah ed, dan bahkan kemudian, itu mungkin tidak terjadi.)

Jika Anda melakukan free suatu mallocwilayah ed, untuk mengetahui apakah ini menyebabkan munmapatau setara (atau brk), Anda harus tahu keadaan internal malloc, serta bagaimana hal itu dilaksanakan. Dan yang ini, tidak seperti yang lain, sangat spesifik platform. (Dan sekali lagi, Anda umumnya harus membatalkan alokasi yang terakhir digunakan mallocdalam suatu mmapsegmen, dan bahkan kemudian, itu mungkin tidak terjadi.)

Jadi, jika Anda ingin memahami mengapa itu terjadi untuk melepaskan tepat 50,5mb, Anda harus melacaknya dari bawah ke atas. Mengapa tidak mallocmemetakan nilai halaman 50,5mb saat Anda melakukan satu freepanggilan atau lebih (untuk mungkin sedikit lebih dari 50,5mb)? Anda harus membaca platform Anda malloc, dan kemudian berjalan di berbagai tabel dan daftar untuk melihat kondisi saat ini. (Pada beberapa platform, bahkan mungkin menggunakan informasi tingkat sistem, yang hampir tidak mungkin ditangkap tanpa membuat snapshot dari sistem untuk memeriksa offline, tetapi untungnya ini biasanya bukan masalah.) Dan kemudian Anda harus lakukan hal yang sama pada 3 level di atas itu.

Jadi, satu-satunya jawaban yang berguna untuk pertanyaan itu adalah "Karena."

Kecuali jika Anda melakukan pengembangan terbatas sumber daya (misalnya tertanam), Anda tidak punya alasan untuk peduli tentang detail ini.

Dan jika Anda sedang melakukan pengembangan sumber daya terbatas, mengetahui rincian ini tidak berguna; Anda cukup banyak harus melakukan end-run di sekitar semua level tersebut dan khususnya mmapmemori yang Anda butuhkan di level aplikasi (mungkin dengan satu pengalokasi zona aplikasi khusus yang sederhana, dipahami dengan baik di antaranya).

abarnert
sumber
2

Pertama, Anda mungkin ingin menginstal lirikan:

sudo apt-get install python-pip build-essential python-dev lm-sensors 
sudo pip install psutil logutils bottle batinfo https://bitbucket.org/gleb_zhulik/py3sensors/get/tip.tar.gz zeroconf netifaces pymdstat influxdb elasticsearch potsdb statsd pystache docker-py pysnmp pika py-cpuinfo bernhard
sudo pip install glances

Kemudian jalankan di terminal!

glances

Dalam kode Python Anda, tambahkan di awal file, berikut ini:

import os
import gc # Garbage Collector

Setelah menggunakan variabel "Besar" (misalnya: myBigVar), Anda ingin melepaskan memori, tuliskan kode python Anda sebagai berikut:

del myBigVar
gc.collect()

Di terminal lain, jalankan kode python Anda dan amati di terminal "lirikan", bagaimana memori dikelola di sistem Anda!

Semoga berhasil!

PS Saya menganggap Anda bekerja pada sistem Debian atau Ubuntu

de20ce
sumber