Zwalnianie pamięci w Pythonie

128

W poniższym przykładzie mam kilka powiązanych pytań dotyczących użycia pamięci.

  1. Jeśli biegnę w tłumacza,

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

    rzeczywista pamięć używana w moim komputerze wzrasta do 80.9mb. Ja wtedy

    del foo

    prawdziwa pamięć spada, ale tylko do 30.4mb. Interpreter używa 4.4mblinii bazowej, więc jaka jest korzyść z nie zwalniania 26mbpamięci do systemu operacyjnego? Czy to dlatego, że Python „planuje z wyprzedzeniem”, myśląc, że możesz ponownie wykorzystać tyle pamięci?

  2. Dlaczego 50.5mbw szczególności uwalnia - na jakiej podstawie jest uwalniana kwota?

  3. Czy istnieje sposób, aby zmusić Pythona do zwolnienia całej używanej pamięci (jeśli wiesz, że nie będziesz ponownie używać takiej ilości pamięci)?

UWAGA To pytanie różni się od pytania Jak jawnie zwolnić pamięć w Pythonie? ponieważ to pytanie dotyczy przede wszystkim zwiększenia użycia pamięci od linii bazowej, nawet po tym, jak interpreter zwolnił obiekty poprzez czyszczenie pamięci (z użyciem gc.collectlub nie).

Jared
źródło
4
Warto zauważyć, że to zachowanie nie jest specyficzne dla Pythona. Zwykle jest tak, że gdy proces zwalnia część pamięci przydzielonej na stercie, pamięć nie jest zwalniana z powrotem do systemu operacyjnego, dopóki proces nie umrze.
NPE
Twoje pytanie dotyczy wielu rzeczy - niektóre z nich są dupkami, niektóre są nieodpowiednie dla SO, a niektóre z nich mogą być dobrymi pytaniami. Pytasz, czy Python nie zwalnia pamięci, w jakich dokładnie okolicznościach może / nie może, jaki jest podstawowy mechanizm, dlaczego został zaprojektowany w ten sposób, czy są jakieś obejścia, czy coś zupełnie innego?
abarnert
2
@abarnert Połączyłem pytania podrzędne, które były podobne. Odpowiadając na twoje pytania: wiem, że Python zwalnia trochę pamięci do systemu operacyjnego, ale dlaczego nie cała i dlaczego tak dużo. Jeśli są okoliczności, w których nie może, dlaczego? Jakie też obejścia.
Jared
@jww Nie sądzę. To pytanie naprawdę dotyczyło tego, dlaczego proces interpretera nigdy nie zwalnia pamięci, nawet po całkowitym zebraniu śmieci z wywołaniami do gc.collect.
Jared

Odpowiedzi:

86

Pamięć przydzielona na stercie może być narażona na znaczniki wysokiego poziomu. Jest to komplikowane przez wewnętrzne optymalizacje Pythona do przydzielania małych obiektów ( PyObject_Malloc) w 4 pulach KiB, sklasyfikowanych pod względem rozmiarów alokacji jako wielokrotności 8 bajtów - do 256 bajtów (512 bajtów w 3.3). Same pule znajdują się na arenach 256 KiB, więc jeśli zostanie użyty tylko jeden blok w jednej puli, cała arena 256 KiB nie zostanie zwolniona. W Pythonie 3.3 alokator małych obiektów został przełączony na używanie anonimowych map pamięci zamiast sterty, więc powinien działać lepiej przy zwalnianiu pamięci.

Ponadto typy wbudowane utrzymują wolne listy wcześniej przydzielonych obiektów, które mogą, ale nie muszą, używać alokatora małych obiektów. intTyp utrzymuje freelist z własnym przydzielonej pamięci i wyczyszczenie go wymaga wywoływania PyInt_ClearFreeList(). Można to nazwać pośrednio, wykonując pełny plik gc.collect.

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

Wynik:

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

Edytować:

Przerzuciłem się na pomiary w odniesieniu do rozmiaru maszyny wirtualnej procesu, aby wyeliminować wpływ innych procesów w systemie.

Środowisko wykonawcze C (np. Glibc, msvcrt) zmniejsza stertę, gdy ciągłe wolne miejsce na górze osiąga stały, dynamiczny lub konfigurowalny próg. Dzięki glibc możesz to dostroić za pomocą mallopt(M_TRIM_THRESHOLD). Biorąc to pod uwagę, nie jest zaskakujące, że sterta skurczy się bardziej - nawet znacznie bardziej - niż blok, który ty free.

W 3.x rangenie tworzy listy, więc powyższy test nie utworzy 10 milionów intobiektów. Nawet jeśli tak, inttyp w 3.x to w zasadzie 2.x long, co nie implementuje listy wolnych.

Eryk Sun
źródło
Użyj memory_info()zamiast get_memory_info()i xjest zdefiniowany
Aziz Alto
Otrzymujesz 10 ^ 7 ints nawet w Pythonie 3, ale każda z nich zastępuje ostatnią zmienną w pętli, więc nie wszystkie istnieją naraz.
Davis Herring,
Spotkałem się z problemem wycieku pamięci i domyślam się, że jest to powód, dla którego tutaj odpowiedziałeś. Ale jak mogę udowodnić moje przypuszczenia? Czy jest jakieś narzędzie, które może pokazać, że wiele basenów jest położonych w środku, ale używany jest tylko mały blok?
ruiruige1991
130

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

Czy istnieje sposób, aby zmusić Pythona do zwolnienia całej używanej pamięci (jeśli wiesz, że nie będziesz ponownie używać takiej ilości pamięci)?

Nie, nie ma. Istnieje jednak łatwe obejście: procesy podrzędne.

Jeśli potrzebujesz 500 MB pamięci tymczasowej przez 5 minut, ale potem musisz pracować przez kolejne 2 godziny i nigdy więcej nie dotkniesz tak dużej ilości pamięci, stwórz proces potomny, aby wykonać pracę wymagającą dużej ilości pamięci. Kiedy proces dziecka odchodzi, pamięć zostaje uwolniona.

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

Po pierwsze, najłatwiejszym sposobem utworzenia procesu potomnego jest użycie concurrent.futures(lub, w przypadku wersji 3.1 i wcześniejszych, futuresbackport w PyPI):

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

Jeśli potrzebujesz trochę większej kontroli, skorzystaj z multiprocessingmodułu.

Koszty są następujące:

  • Uruchamianie procesu jest trochę powolne na niektórych platformach, zwłaszcza w systemie Windows. Mówimy tutaj o milisekundach, a nie minutach, a jeśli nakręcisz jedno dziecko do wykonania 300 sekund pracy, nawet tego nie zauważysz. Ale to nie jest darmowe.
  • Jeśli duża ilość pamięci tymczasowej, której używasz, jest naprawdę duża , może to spowodować zamianę głównego programu. Oczywiście na dłuższą metę oszczędzasz czas, ponieważ gdyby ta pamięć wisiała na zawsze, musiałaby w pewnym momencie doprowadzić do zamiany. Ale może to zmienić stopniowe spowolnienie w bardzo zauważalne (i wczesne) opóźnienia w niektórych przypadkach użycia.
  • Przesyłanie dużych ilości danych między procesami może być powolne. Ponownie, jeśli mówisz o wysłaniu ponad 2 tys. Argumentów i odzyskaniu 64 tys. 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 (plik, mmapped lub w inny sposób; interfejsy API pamięci współdzielonej w multiprocessing; itp.).
  • Przesyłanie dużych ilości danych między procesami oznacza, że ​​dane muszą być zdolne do zapakowania (lub, jeśli umieścisz je w pliku lub pamięci współdzielonej, - structlub najlepiej - ctypes).
abarnert
źródło
Naprawdę miły podstęp, choć nie rozwiązując problem :( ale naprawdę podoba
ddofborg
32

eryksun odpowiedział na pytanie nr 1, a ja odpowiedziałem na pytanie nr 3 (oryginalne nr 4), ale teraz odpowiedzmy na pytanie nr 2:

Dlaczego w szczególności uwalnia 50,5 MB - na jakiej podstawie jest uwalniana kwota?

Ostatecznie opiera się na całej serii zbiegów okoliczności w Pythonie, mallocktóre są bardzo trudne do przewidzenia.

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

Lub możesz mierzyć strony w użyciu, które mogą liczyć strony przydzielone, ale nigdy nie dotknięte (w systemach, które optymistycznie przydzielają zbyt dużo, takich jak linux), strony, które są przydzielone, ale oznaczone MADV_FREEitp.

Jeśli naprawdę mierzysz przydzielone strony (co w rzeczywistości nie jest bardzo przydatne, ale wydaje się, że właśnie o to pytasz), a strony zostały naprawdę cofnięte, są to dwie okoliczności, w których może się to zdarzyć: albo użyłeś brklub równoważnego do zmniejszenia segmentu danych (obecnie bardzo rzadko) lub użyłeś munmaplub podobnego do zwolnienia zmapowanego segmentu. (Teoretycznie istnieje również niewielki wariant tego ostatniego, ponieważ istnieją sposoby na uwolnienie części zmapowanego segmentu - np. Kradzież go MAP_FIXEDza pomocą MADV_FREEsegmentu, którego natychmiast usuwasz).

Jednak większość programów nie alokuje bezpośrednio rzeczy ze stron pamięci; używają mallocalokatora w stylu. Kiedy wywołujesz free, alokator może zwolnić strony do systemu operacyjnego tylko wtedy, gdy akurat jesteś freeostatnim aktywnym obiektem w mapowaniu (lub na ostatnich N stronach segmentu danych). Nie ma możliwości, aby Twoja aplikacja mogła to przewidzieć, a nawet wykryć, że stało się to z wyprzedzeniem.

CPython jeszcze bardziej komplikuje to zadanie - ma niestandardowy, dwupoziomowy alokator obiektów, znajdujący się na szczycie niestandardowego alokatora pamięci malloc. ( Bardziej szczegółowe wyjaśnienie można znaleźć w komentarzach do źródeł ). Ponadto, nawet na poziomie C API, a tym bardziej w Pythonie, nie można nawet bezpośrednio kontrolować, kiedy obiekty najwyższego poziomu są zwalniane.

Tak więc, kiedy zwalniasz obiekt, skąd wiesz, czy zwolni on pamięć do systemu operacyjnego? Cóż, najpierw musisz wiedzieć, że zwolniłeś ostatnie odniesienie (w tym wszelkie odniesienia wewnętrzne, o których nie wiedziałeś), pozwalając GC na zwolnienie go. (W przeciwieństwie do innych implementacji, przynajmniej CPython zwalnia obiekt, gdy tylko jest to dozwolone). Zwykle zwalnia to co najmniej dwie rzeczy na następnym poziomie niższym (np. W przypadku łańcucha zwalniasz PyStringobiekt, a bufor ciągu ).

Jeśli robić zwalnianie obiektu, aby wiedzieć, czy to powoduje, że następny poziom niżej do deallocate blok przechowywania obiektów, trzeba znać stan wewnętrzny podzielnika obiektu, a także jak to jest realizowane. (Oczywiście nie może się to zdarzyć, chyba że zwolnisz ostatnią rzecz w bloku, a nawet wtedy może się to nie zdarzyć).

Jeśli zrobić deallocate blok przechowywania obiektów, aby wiedzieć, czy powoduje to freewezwanie, trzeba znać stan wewnętrzny podzielnika PyMem, a także jak to jest realizowane. (Ponownie, musisz cofnąć przydział ostatniego używanego bloku w mallocregionie ed, a nawet wtedy może się to nie zdarzyć).

Jeśli zrobić free to mallocregion, ED, aby wiedzieć, czy powoduje to munmaplub jego odpowiednik (lub brk), trzeba znać stan wewnętrznej malloc, a także jak to jest realizowane. A ten, w przeciwieństwie do innych, jest wysoce specyficzny dla platformy. (I znowu, generalnie musisz cofnąć przydział ostatniego używanego mallocw mmapsegmencie, a nawet wtedy może się to nie zdarzyć).

Więc jeśli chcesz zrozumieć, dlaczego wydarzyło się dokładnie 50,5 MB, będziesz musiał prześledzić to od dołu do góry. Dlaczego mallocodmapowano strony o wartości 50,5 MB, gdy wykonałeś te jedno lub więcej freepołączeń (prawdopodobnie dla nieco ponad 50,5 MB)? Musisz przeczytać platformę malloc, 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órych prawie niemożliwe jest przechwycenie bez wykonania migawki systemu w celu sprawdzenia w trybie offline, ale na szczęście zwykle nie stanowi to problemu). A potem musisz zrób to samo na 3 poziomach powyżej.

Tak więc jedyną przydatną odpowiedzią na to pytanie jest „Ponieważ”.

Jeśli nie wykonujesz programowania z ograniczeniem zasobów (np. Osadzonym), nie masz powodu, aby przejmować się tymi szczegółami.

A jeśli zajmujesz się rozwojem z ograniczonymi zasobami, znajomość tych szczegółów jest bezużyteczna; w zasadzie trzeba wykonać końcowe omówienie wszystkich tych poziomów, a konkretnie mmappotrzebnej pamięci na poziomie aplikacji (być może z jednym prostym, dobrze zrozumiałym alokatorem stref specyficznym dla aplikacji pomiędzy).

abarnert
źródło
2

Najpierw możesz zainstalować spojrzenia:

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

Następnie uruchom go w terminalu!

glances

W kodzie Pythona dodaj na początku pliku:

import os
import gc # Garbage Collector

Po użyciu zmiennej „Big” (na przykład: myBigVar), dla której chciałbyś zwolnić pamięć, napisz w swoim kodzie Pythona:

del myBigVar
gc.collect()

Na innym terminalu uruchom swój kod w Pythonie i obserwuj w terminalu „spojrzenia”, jak zarządzana jest pamięć w Twoim systemie!

Powodzenia!

PS Zakładam, że pracujesz na systemie Debian lub Ubuntu

de20ce
źródło