2013-03-16 12 views
94

Mam kilka powiązanych pytań dotyczących użycia pamięci w następującym przykładzie.Uwalnianie pamięci w języku Python

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

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

    prawdziwa pamięć używana na moim komputerze podchodzi do 80.9mb. Później pamięć zanika, ale tylko do 30.4mb. Interpreter korzysta z linii bazowej 4.4mb, więc jaka jest korzyść z nieudzielania pamięci do systemu operacyjnego przez 26mb? Czy to dlatego, że Python "planuje naprzód", myśląc, że możesz znowu użyć tyle pamięci?

  2. Dlaczego w szczególności publikuje 50.5mb - na jakiej wysokości jest wydana?

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

+3

Warto zauważyć, że to zachowanie nie jest specyficzne dla Pythona. Zwykle, gdy proces zwalnia pamięć przydzieloną stertom, pamięć nie zostaje zwolniona z powrotem do systemu operacyjnego, dopóki proces nie zakończy się. – NPE

+0

Twoje pytanie wymaga wielu rzeczy - niektóre z nich to dupki, z których niektóre są nieodpowiednie dla SO, a niektóre z nich mogą być dobrymi pytaniami. Czy pytasz, czy Python nie zwalnia pamięci, pod ściśle określonymi okolicznościami, które 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, które były podobne. Aby odpowiedzieć na twoje pytania: Wiem, że Python wydaje trochę pamięci systemowi operacyjnemu, ale dlaczego nie wszystkie i dlaczego to robi. Jeśli istnieją okoliczności, w których nie może, dlaczego? Co też obejdzie. – Jared

Odpowiedz

70

Pamięć przydzielona do sterty może podlegać znacznikom wody. Jest to skomplikowane przez wewnętrzne optymalizacje Pythona dotyczące przydzielania małych obiektów (PyObject_Malloc) w pulach 4 KiB, klasyfikowanych dla wielkości alokacji wielokrotności 8 bajtów - do 256 bajtów (512 bajtów w 3.3). Same baseny są w 256 KiB arenach, 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 mały alokator obiektów został przełączony na anonimowe mapy pamięci zamiast sterty, więc powinien działać lepiej przy zwalnianiu pamięci.

Ponadto wbudowane typy zachowują wolne listy wcześniej przydzielonych obiektów, które mogą lub nie mogą korzystać z małego obiektu przydzielania. Typ int utrzymuje wolną listę z przypisaną jej pamięcią, a jej usunięcie wymaga wywołania PyInt_ClearFreeList(). Można to nazwać pośrednio, wykonując pełny kod gc.collect.

Spróbuj tego i powiedz, co otrzymujesz. 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ściowa:

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

Edit:

przeszedłem do pomiaru względem wielkości procesowej VM w celu wyeliminowania skutków innych procesów w systemie.

Środowisko wykonawcze C (np. Glibc, msvcrt) zmniejsza stertę, gdy przyległa wolna przestrzeń u góry osiąga stały, dynamiczny lub konfigurowalny próg. Za pomocą glibc możesz nastroić to za pomocą mallopt (M_TRIM_THRESHOLD). Biorąc to pod uwagę, nie jest zaskakujące, że strop kurczy się o więcej - nawet o wiele więcej - niż ten blok, który jest free.

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

90

Zgaduję pytanie troszczą się o to tutaj:

Czy istnieje sposób, aby zmusić Pythona, aby zwolnić całą pamięć, która została użyta (jeśli wiesz, że nie będzie używać tyle pamięć ponownie)?

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

Jeśli potrzebujesz 500 MB tymczasowej pamięci masowej przez 5 minut, ale potem będziesz musiał uruchomić przez kolejne 2 godziny i nigdy więcej nie dotkniesz pamięci, odtwórz proces potomny, aby wykonać intensywną pracę z pamięcią. Gdy proces potomny zniknie, pamięć zostanie zwolniona.

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

Po pierwsze, to najprostszy sposób, aby utworzyć procesu potomnego jest z concurrent.futures (lub na 3.1 i wcześniej, futures backport na PyPI):

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

Jeśli potrzebujemy trochę więcej kontroli, użyj multiprocessing moduł.

Koszty są:

  • Proces uruchamiania jest trochę powolny na niektórych platformach, zwłaszcza w systemie Windows. Mówimy tu o milisekundach, a nie minutach, a jeśli kręcisz jedno dziecko, aby wykonać 300 sekund pracy, nawet go 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 Twój główny program zostanie zamieniony. Oczywiście oszczędzasz czas na dłuższą metę, ponieważ gdyby pamięć ta była zawieszona na zawsze, musiałaby w pewnym momencie doprowadzić do zamiany. Ale może to stopniowo zmniejszyć spowolnienie w bardzo zauważalne jednorazowe (i wczesne) opóźnienia w niektórych przypadkach użycia.
  • Wysyłanie dużych ilości danych między procesami może być wolne. Ponownie, jeśli mówisz o wysyłaniu 2K argumentów i uzyskiwaniu 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, mmap ped lub w inny sposób, interfejsy API pamięci współużytkowanej w multiprocessing; itp.).
  • Wysyłanie dużych ilości danych między procesami oznacza, że ​​dane muszą być łatwe do zmieszczenia (lub, jeśli umieścisz je w pliku lub pamięci współdzielonej, struct -able lub idealnie ctypes -able).
+12

Ktokolwiek potępił, chciałby wyjaśnić, dlaczego? – abarnert

+0

Naprawdę fajna sztuczka, chociaż nie rozwiązuje problemu :(Ale bardzo mi się podoba – ddofborg

24

eryksun odpowiedział na pytanie nr 1, a ja już odpowiedział na pytanie nr 3 (oryginalna # 4), ale teraz niech odpowiedź pytanie nr 2:

Dlaczego zwolnić 50.5mb w szczególności - na jaką kwotę jest wydana?

To, na czym się opiera, to w końcu cała seria zbiegów okoliczności w Pythonie i malloc, które są bardzo trudne do przewidzenia.

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

Lub możesz mierzyć strony w użyciu, które mogą lub nie mogą liczyć przydzielone, ale nigdy nie dotknięte strony (w systemach, które optymistycznie przesadzają, jak linux), strony, które są przydzielane, ale oznaczone MADV_FREE, itp.

Jeśli naprawdę mierzysz przydzielone strony (co w rzeczywistości nie jest bardzo przydatną rzeczą do zrobienia, ale wydaje się, że o to pytasz), a strony zostały faktycznie zwolnione, dwie okoliczności, w których to może happen: Albo użyłeś brk lub odpowiednika, aby zmniejszyć segment danych (bardzo rzadko w dzisiejszych czasach) lub użyłeś munmap lub podobnego, aby zwolnić zmapowany segment. (Jest też teoretycznie drobne wariant do tego ostatniego, że istnieją sposoby, aby zwolnić część odwzorowanym segmencie-np ukraść z MAP_FIXED dla MADV_FREE segmentu, który natychmiast unmap.)

Jednak większość programów nie bezpośrednio alokować rzeczy ze stron pamięci; używają przydziału w stylu malloc. Podczas wywoływania free program przydzielający może tylko zwolnić strony do systemu operacyjnego, jeśli akurat w rzeczywistości jest to free ostatni obiekt na żywo w odwzorowaniu (lub na ostatnich N stronach segmentu danych). Nie ma możliwości, aby aplikacja mogła to przewidzieć, a nawet wykryć, że miało to miejsce z góry.

CPython czyni to jeszcze bardziej skomplikowanym - ma niestandardowy 2-poziomowy alokator obiektów na wierzchu niestandardowego alokatora pamięci na szczycie malloc. (Aby uzyskać bardziej szczegółowe wyjaśnienie, patrz: the source comments). Co więcej, nawet na poziomie API C, znacznie mniej Python, nie kontrolujesz nawet bezpośrednio, kiedy obiekty najwyższego poziomu są przydzielone.

Po zwolnieniu obiektu, skąd wiadomo, czy zamierza zwolnić pamięć w systemie operacyjnym? Cóż, najpierw musisz wiedzieć, że wydałeś ostatnie referencje (w tym wewnętrzne referencje, o których nie wiedziałeś), pozwalając GC je zwolnić. (W przeciwieństwie do innych implementacji, przynajmniej CPython zwolni obiekt, gdy tylko na to pozwoli.) Zazwyczaj zwalnia co najmniej dwie rzeczy na niższym poziomie (np. Dla ciągu znaków zwalniasz obiekt PyString, a ciąg znaków bufor).

Jeśli zrobić zwalnianie obiektu, aby wiedzieć, czy to powoduje, że następny poziom w dół, aby cofnąć przydział bloku przechowywania obiektów, trzeba znać stan wewnętrzny podzielnika obiektu, a także jak to jest realizowane. (To oczywiście nie może się zdarzyć, dopóki nie zwolnisz ostatniej rzeczy w bloku, a nawet wtedy, może się to nie zdarzyć.)

Jeśli chcesz zrobić zwolnić blok pamięci obiektów, aby dowiedzieć się, czy to powoduje wywołanie free, musisz znać stan wewnętrzny programu przydzielającego PyMem, a także sposób jego implementacji. (Ponownie, musisz być dealokując ostatni blok w użyciu w regionie malloc ED, a nawet jeśli, to nie może się zdarzyć.)

Jeśli zrobićfree region malloc ed, aby wiedzieć, czy to powoduje munmap lub odpowiednik (lub brk), musisz znać stan wewnętrzny malloc, a także sposób jego implementacji. I ten, w przeciwieństwie do innych, jest wysoce specyficzny dla platformy. (I znowu, zazwyczaj musisz zwolnić ostatni w użyciu malloc w segmencie mmap, a nawet wtedy może się to nie zdarzyć.)

Więc, jeśli chcesz zrozumieć, dlaczego tak się stało, wypuść dokładnie 50,5 MB , będziesz musiał wyśledzić to od dołu do góry.Dlaczego malloc odrzuciła 50,5 miliona stron, gdy wykonałeś jedno lub więcej wywołań free (prawdopodobnie nieco więcej niż 50,5 mb)? Musisz przeczytać swoją platformę malloc, a następnie przejrzeć różne tabele i listy, aby zobaczyć jej bieżący stan. (Na niektórych platformach może nawet wykorzystywać informacje na poziomie systemu, które są prawie niemożliwe do uchwycenia bez zrobienia migawki systemu w celu sprawdzenia w trybie offline, ale na szczęście nie jest to zazwyczaj problem.) A następnie musisz zrób to samo na 3 poziomach powyżej.

Jedyna użyteczna odpowiedź na to pytanie brzmi "Ponieważ".

O ile nie zajmujesz się tworzeniem ograniczonych zasobów (np. Osadzonych), nie masz powodu dbać o te szczegóły.

A jeśli jesteś , jesteś przy tworzeniu ograniczonych zasobów, poznanie tych szczegółów jest bezużyteczne; musisz wykonać końcowy test na wszystkich tych poziomach, a konkretnie na mmap pamięci, której potrzebujesz na poziomie aplikacji (być może z jednym prostym, dobrze zrozumianym, podzielonym na strefy interfejsem pomiędzy).

+0

Nie wiedziałem, że mogę opublikować wiele odpowiedzi ... – laike9m