Zwalnianie pamięci w Pythonie

Mam kilka pytań dotyczących wykorzystania pamięci w poniższym przykładzie.

  1. Jeśli uruchomię interpreter,

    foo = ['bar' for _ in xrange(10000000)]
    
    Prawdziwa pamięć użyta na mojej maszynie sięga 80.9mb. I wtedy,
    del foo
    
    Prawdziwa pamięć zanika, ale tylko do 30.4mb. Interpreter korzysta z linii bazowej 4.4mb, więc jaka jest zaleta nie wypuszczania 26mb pamięci do systemu operacyjnego? Czy to dlatego, że Python "planuje z wyprzedzeniem", myśląc, że możesz użyć tyle pamięci znowu?
  2. Dlaczego w szczególności wypuszcza 50.5mb - na jakiej podstawie jest wydawana kwota?

  3. Czy istnieje sposób, aby zmusić Pythona do uwolnienia całej pamięci, która została użyta (jeśli wiesz, że nie będziesz używać więcej pamięci)?

Author: Jared, 2013-03-17

3 answers

Pamięć przydzielona na stercie może podlegać znacznikom o wysokiej zawartości wody. Jest to skomplikowane przez wewnętrzne optymalizacje Pythona do przydzielania małych obiektów (PyObject_Malloc) w 4 basenach KiB, klasyfikowanych dla wielkości alokacji w wielokrotnościach 8 bajtów-do 256 bajtów (512 bajtów w 3.3). Same baseny znajdują się na arenach 256 KiB, więc jeśli tylko jeden blok w jednej puli zostanie użyty, Cała arena 256 KiB nie zostanie zwolniona. W Pythonie 3.3 alokator małych obiektów został zamieniony na anonimowe mapy pamięci sterty, więc powinno działać lepiej na zwalnianie pamięci.

Dodatkowo, wbudowane typy utrzymują freelistów wcześniej przydzielonych obiektów, które mogą lub nie mogą używać alokatora małych obiektów. Typ int utrzymuje freelist z własną przydzieloną pamięcią, a wyczyszczenie go wymaga wywołania PyInt_ClearFreeList(). Można to wywołać pośrednio, wykonując pełną gc.collect.

Spróbuj w ten sposób i powiedz mi, co masz. Oto link do psutil .
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)

Wyjście:

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

Edit:

Przełączyłem się na pomiar w stosunku do rozmiaru maszyny wirtualnej procesu, aby wyeliminować efekty innych procesów w systemie.

Glibc, msvcrt) zmniejsza stertę, gdy ciągła wolna przestrzeń na górze osiągnie stały, dynamiczny lub konfigurowalny próg. W glibc możesz to dostroić mallopt (M_TRIM_THRESHOLD). Biorąc to pod uwagę, nie jest zaskakujące, jeśli sterta kurczy się o więcej -- nawet o wiele więcej ... niż blok, który ty free.

W 3.x range nie tworzy listy, więc powyższy test nie utworzy 10 milionów int obiektów. Nawet jeśli tak, to int wpisz w 3.x to w zasadzie 2.x long, który nie implementuje freelisty.

 74
Author: eryksun,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-03-19 05:53:01

Zgaduję, że pytanie, na którym naprawdę Ci zależy, brzmi:

Czy istnieje sposób, aby zmusić Pythona do uwolnienia całej pamięci, która została użyta (jeśli wiesz, że nie będziesz używać więcej pamięci)?

Nie, Nie ma. Istnieje jednak łatwe obejście: procesy potomne.

Jeśli potrzebujesz 500MB pamięci tymczasowej przez 5 minut, ale po tym musisz uruchomić przez kolejne 2 godziny i nie dotkniesz więcej pamięci, uruchom proces potomny, aby zrobić pamięć-intensywna praca. Gdy proces potomny odchodzi, pamięć zostaje uwolniona.

Nie jest to całkowicie trywialne i bezpłatne, ale jest dość łatwe i tanie, co zwykle jest wystarczająco dobre, aby handel był opłacalny.

Po pierwsze, najprostszym sposobem utworzenia procesu potomnego jest concurrent.futures (lub, dla 3.1 i wcześniejszych,futures backport na PyPI):

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

Jeśli potrzebujesz trochę więcej kontroli, użyj multiprocessing Moduł.

Koszty są:

  • uruchamianie procesu jest trochę powolne na niektórych platformach, zwłaszcza Windows. Mówimy tu o milisekundach, a nie minutach, a jeśli nakręcasz jedno dziecko na 300 sekund pracy, nawet tego nie zauważysz. Ale to nie jest za darmo.
  • jeśli duża ilość pamięci tymczasowej, której używasz, naprawdę jest duża , może to spowodować, że główny program zostanie zamieniony. Oczywiście oszczędzasz czas na dłuższą metę, bo jeśli to wspomnienie wisiało na zawsze to w pewnym momencie musiałby doprowadzić do wymiany. Ale to może przekształcić stopniowe spowolnienie w bardzo zauważalne all-at-once (i wczesne) opóźnienia w niektórych przypadkach użycia.
  • wysyłanie dużych ilości danych między procesami może być powolne. Ponownie, jeśli mówisz o wysłaniu ponad 2K argumentów i odzyskaniu 64K wyników, nawet tego nie zauważysz, ale jeśli wysyłasz i odbierasz duże ilości danych, będziesz chciał użyć innego mechanizmu (pliku, mmapped lub w inny sposób; interfejsy API pamięci współdzielonej w multiprocessing; itd.).
  • przesyłanie dużych ilości danych między procesami oznacza, że dane muszą być zbierane (lub, jeśli umieścisz je w pliku lub pamięci współdzielonej, struct - able lub najlepiej ctypes - able).
 101
Author: abarnert,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-03-19 06:05:34

Eryksun odpowiedział na pytanie #1, A ja odpowiedziałem na pytanie #3 (oryginał #4), ale teraz odpowiedzmy na pytanie # 2:

Dlaczego w szczególności wypuszcza 50,5 mb - na jakiej podstawie jest wydana kwota?

To, na czym opiera się, to ostatecznie cała seria zbiegów okoliczności wewnątrz Pythona i malloc, które są bardzo trudne do przewidzenia.

Po pierwsze, w zależności od tego, jak mierzysz pamięć, możesz mierzyć tylko strony faktycznie zmapowane na pamięć. W takim przypadku za każdym razem, gdy strona zostanie zamieniona przez pager, pamięć pojawi się jako "uwolniona", nawet jeśli nie została uwolniona.

Lub możesz mierzyć strony w użyciu, które mogą lub nie mogą liczyć stron przydzielonych, ale nigdy nie dotkniętych (w systemach, które optymistycznie nadalokują, takich jak linux), stron przydzielonych, ale oznaczonych MADV_FREE, itp.

Jeśli naprawdę mierzysz przydzielone strony (co w rzeczywistości nie jest zbyt przydatną rzeczą do zrobienia, ale wydaje się, że jest to, co w dwóch okolicznościach może się to zdarzyć: albo użyłeś brk lub równoważnego skrócenia segmentu danych (bardzo rzadko w dzisiejszych czasach), albo użyłeś munmap lub podobnego do wydania zmapowanego segmentu. (Istnieje również teoretycznie mniejszy wariant tego ostatniego, ponieważ istnieją sposoby na uwolnienie części zmapowanego segmentu-np. kradzież go za pomocą MAP_FIXED dla MADV_FREE segmentu, który natychmiast odmapujesz.)

Ale większość programów nie przydziela bezpośrednio rzeczy poza stronami pamięci; używają alokatora w stylu malloc. Gdy wywołujesz free, alokator może zwolnić strony do systemu operacyjnego tylko wtedy, gdy akurat znajdujesz się freejako ostatni aktywny obiekt w mapowaniu (lub na ostatnich n stronach segmentu danych). Nie ma możliwości, aby Twoja aplikacja mogła rozsądnie przewidzieć to, a nawet wykryć, że stało się to z wyprzedzeniem.

CPython czyni to jeszcze bardziej skomplikowanym-ma niestandardowy 2-poziomowy alokator obiektów na górze niestandardowego alokatora pamięci na górze malloc. (Zobacz komentarze źródłowe dla bardziej szczegółowego wyjaśnienia.) I na dodatek, nawet na poziomie C API, a tym bardziej Pythona, nie można nawet bezpośrednio kontrolować, kiedy obiekty najwyższego poziomu są dealokowane.

Więc, kiedy zwalniasz obiekt, skąd wiesz, czy zwolni on pamięć do systemu operacyjnego? Cóż, najpierw musisz wiedzieć, że wydałeś ostatnią referencję (w tym wszelkie wewnętrzne odniesienia, o których nie wiedziałeś), pozwalając GC na jej deallokację. (W przeciwieństwie do innych implementacji, przynajmniej CPython dealokuje obiekt tak szybko, jak to możliwe.) To zwykle deallokuje co najmniej dwie rzeczy na następnym poziomie w dół(np. w przypadku łańcucha znaków zwalniasz obiekt PyString i bufor łańcuchów).

Jeśli wykonasz dealokację obiektu, aby dowiedzieć się, czy spowoduje to dealokację bloku pamięci obiektowej na następnym poziomie, musisz znać wewnętrzny stan alokatora obiektów, a także sposób jego implementacji. (Oczywiście nie może zdarza się, chyba że dealokujesz ostatnią rzecz w bloku, a nawet wtedy, może się nie zdarzyć.)

Jeśli wykonasz dealokację bloku object storage, aby wiedzieć, czy powoduje to wywołanie free, musisz znać wewnętrzny stan alokatora PyMem, jak również sposób jego implementacji. (Ponownie musisz dealokować ostatni używany blok w obrębie malloced, a nawet wtedy może się to nie zdarzyć.)

If you do free A mallocED region, aby wiedzieć, czy to powoduje {[3] } lub równoważny (lub brk), musisz znać wewnętrzny stan malloc, a także sposób jego implementacji. A ten, w przeciwieństwie do innych, jest bardzo specyficzny dla platformy. (I znowu, generalnie musisz dealokować ostatni używany malloc w obrębie mmap segmentu, a nawet wtedy może się to nie zdarzyć.)

Więc jeśli chcesz zrozumieć, dlaczego wydało się dokładnie 50,5 mb, będziesz musiał prześledzić to od dołu do góry. Dlaczego malloc unmap 50.5 mb warto stron, kiedy wykonałeś te jedno lub więcej free połączeń (na prawdopodobnie nieco ponad 50,5 mb)? Musisz przeczytać malloc swojej platformy, a następnie przejść przez różne tabele i listy, aby zobaczyć jej aktualny stan. (Na niektórych platformach może nawet wykorzystywać informacje na poziomie systemu, które są prawie niemożliwe do przechwycenia bez zrobienia migawki systemu w celu sprawdzenia w trybie offline, ale na szczęście zwykle nie stanowi to problemu.) I wtedy trzeba zrobić to samo na 3 poziomach powyżej to.

Więc jedyną użyteczną odpowiedzią na pytanie jest "ponieważ."

Jeśli nie zajmujesz się tworzeniem ograniczonych zasobów (np. osadzonych), nie masz powodu, aby dbać o te szczegóły.

A jeśli zajmujesz się rozwojem ograniczonym do zasobów, znajomość tych szczegółów jest bezużyteczna; musisz wykonać end-run na wszystkich tych poziomach, a konkretnie mmap pamięć, której potrzebujesz na poziomie aplikacji (prawdopodobnie z jednym prostym, dobrze zrozumiałym, alokator strefowy specyficzny dla aplikacji).

 27
Author: abarnert,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-03-19 19:04:01