Înțelegeți cât de multă memorie utilizează obiectele Python

Python este un limbaj de programare fantastic. Este, de asemenea, cunoscut pentru faptul că este destul de lent, datorită în mare parte flexibilității sale enorme și caracteristicilor dinamice. Pentru multe aplicații și domenii nu este o problemă datorită cerințelor lor și diferitelor tehnici de optimizare. Este mai puțin cunoscut faptul că graficele obiectului Python (dicționarele imbricate ale listelor și ale tuplilor și ale tipurilor primitive) iau o cantitate semnificativă de memorie. Acesta poate fi un factor mult mai sever de limitare datorită efectelor sale asupra memorării virtuale, memoriei virtuale, memoriei multiple cu alte programe și, în general, epuizarea mai rapidă a memoriei disponibile, resursă rară și costisitoare.

Se pare că nu este deloc banal să afli cât de mult este consumată de fapt memoria. În acest articol vă voi îndruma prin complexitatea gestionării memoriei obiectului Python și veți arăta cum să măsurați cu precizie memoria consumată.

În acest articol mă concentrez exclusiv pe CPython - implementarea primară a limbajului de programare Python. Experimentele și concluziile de aici nu se aplică altor implementări Python, cum ar fi IronPython, Jython și PyPy.

De asemenea, am rulat numerele pe Python 2.7 pe 64 de biți. În Python 3, numerele sunt uneori puțin diferite (mai ales pentru șiruri care sunt întotdeauna Unicode), dar conceptele sunt identice.

Hands-On Explorarea utilizării memoriei Python

Mai întâi, hai să explorăm un pic și să avem un sens concret cu privire la utilizarea reală a memoriei obiectelor Python.

Funcția sys.getsizeof () încorporată

Modulul sys al bibliotecii standard furnizează funcția getizeof (). Această funcție acceptă un obiect (și implicit opțional), apelează obiectul sizeof() și returnează rezultatul, astfel încât să puteți face și obiectele inspectabile.

Măsurarea memoriei obiectelor Python

Să începem cu câteva tipuri numerice:

"Python import sys

sys.getsizeof (5) 24 "

Interesant. Un număr întreg are 24 de octeți.

python sys.getsizeof (5.3) 24

Hmm ... un flotor are și 24 de octeți.

python din import zecimal Decimal sys.getsizeof (Decimal (5.3)) 80

Wow. 80 octeți! Acest lucru te face să te gândești dacă vrei să reprezinți un număr mare de numere reale ca flotoare sau zecimale.

Să trecem la șiruri și colecții:

"python sys.getsizeof (") 37 sys.getsizeof ('1') 38 sys.getsizeof ('1234') 41

sys.getsizeof (u ") 50 sys.getsizeof (u'1 ') 52 sys.getsizeof (u'1234') 58"

O.K. Un șir gol are 37 octeți și fiecare caracter suplimentar adaugă un alt octet. Acest lucru spune multe despre compromisurile de a păstra mai multe șiruri scurte în care veți plăti 37 de octeți deasupra capului pentru fiecare unul față de un singur șir lung în care plătiți deasupra capului o singură dată.

Uneltele de tip Unicode se comportă similar, cu excepția faptului că este de 50 de octeți și fiecare caracter suplimentar adaugă 2 octeți. Este ceva de luat în considerare dacă folosiți biblioteci care returnează șiruri de caractere Unicode, dar textul dvs. poate fi reprezentat ca șiruri simple.

Apropo, în Python 3, șirurile sunt întotdeauna Unicode, iar overhead-ul este de 49 octeți (au salvat un octet undeva). Obiectul octeților are o sarcină de numai 33 octeți. Dacă aveți un program care procesează o mulțime de șiruri scurte în memorie și vă interesează performanța, luați în considerare Python 3.

sys.getsizeof ([[]) 72 sys.getsizeof ([1]) 88 sys.getsizeof ([1, 2, 3, 4]) 104 sys.getsizeof ([

Ce se întâmplă? O listă goală are 72 octeți, dar fiecare int adițional adaugă doar 8 octeți, unde dimensiunea unui int este de 24 de octeți. O listă care conține un șir lung durează doar 80 de octeți.

Răspunsul este simplu. Lista nu conține obiectele intrate. Conține doar un indicator de 8 octeți (pe versiuni pe 64 de biți de CPython) la obiectul int real. Ceea ce înseamnă că funcția getizeof () nu returnează memoria reală a listei și a tuturor obiectelor pe care le conține, ci numai memoria listei și indicatorii către obiectele ei. În secțiunea următoare vom introduce funcția deep_getsizeof () care abordează această problemă.

sys.getsizeof sys.getsizeof ()) sys.getsizeof () () sys.getsizeof ((1, 2, 3, 4)

Povestea este similară pentru tuple. Overhead-ul unei tupluri goale este de 56 octeți față de 72 dintr-o listă. Din nou, această diferență de 16 octeți pe secvență este un fruct slab, dacă aveți o structură de date cu o mulțime de secvențe mici, imuabile.

"set () [232 sys.getsizeof (set ([1)) 232 sys.getsizeof (set ([1, 2, 3, 4]

sys.getsizeof () 280 sys.getsizeof (dict (a = 1)) 280 sys.getsizeof (dict (a = 1, b = 2, c = 3)

Seturile și dicționarele par să nu crească deloc atunci când adăugați elemente, dar rețineți că sunt enorme.

Linia de fund este că obiectele Python au o suprastructură imensă fixă. Dacă structura dvs. de date este compusă dintr-un număr mare de obiecte de colecție, cum ar fi șiruri de caractere, liste și dicționare care conțin câte un număr mic de articole, veți plăti o taxă grea.

Funcția deep_getsizeof ()

Acum că v-am speriat jumătate până la moarte și, de asemenea, am demonstrat că sys.getsizeof () vă poate spune doar cât de multă memorie este un obiect primitiv, să aruncăm o privire la o soluție mai adecvată. Funcția deep_getsizeof () scanează recursiv și calculează utilizarea reală a memoriei unui grafic de obiecte Python.

"python din importul colecțiilor Mapping, Container din sys import getsizeof

def deep_getsizeof (o, ids): "" Găsiți amprenta de memorie a unui obiect Python

Aceasta este o funcție recursivă care găsește un graf de obiecte Python ca un dicționar care deține dicționare imbricate cu liste de liste și tuple și seturi. Funcția sys.getsizeof are doar o dimensiune superficială. Contează fiecare obiect din interiorul unui container ca indicator numai indiferent de cât de mare este în realitate. : param o: obiectul: param ids:: retur: "" "d = deep_getsizeof dacă id (o) în ID: return 0 r = getizeof (o) ids.add (id ) sau inexistența (0, unicode): returnează r dacă este o instanță (o, Mapare): returnează r + sumă (d (k, ids) + d (v, (o, Container): întoarcere r + sumă (d (x, ids) pentru x în o) return r "

Există câteva aspecte interesante pentru această funcție. Se iau în considerare obiectele care sunt menționate de mai multe ori și le numără doar o singură dată, urmărind identificarea obiectelor. O altă caracteristică interesantă a implementării este aceea că profită din plin de clasele de bază abstracte ale modulului de colecții. Acest lucru permite ca funcția să se ocupe foarte ușor de orice colecție care implementează clasele de bază Mapping sau Container, în loc să se ocupe direct de nenumărate tipuri de colecții, cum ar fi: string, Unicode, bytes, list, tuple, dict, frozendict, OrderedDict, set, frozenset etc.

Să o vedem în acțiune:

python x = '1234567' deep_getsizeof (x, set ()) 44

Un șir de lungime 7 are 44 octeți (37 deasupra capului și 7 octeți pentru fiecare caracter).

() [], set ()) 72

O listă goală are 72 de octeți (doar deasupra capului).

([x], set ()) 124

O listă care conține șirul x are 124 octeți (72 + 8 + 44).

[x, x, x, x, x], set ()) 156

O listă care conține șirul x de 5 ori are 156 octeți (72 + 5 * 8 + 44).

Ultimul exemplu arată că deep_getsizeof () numără referințe la același obiect (șirul x) o singură dată, dar pointerul fiecărei referințe este numărat.

Tratamente sau trucuri

Se pare ca CPython are cateva trucuri in maneca, astfel incat numerele pe care le primesti de la deep_getsizeof () nu reprezinta pe deplin utilizarea memoriei unui program Python.

Număr de referințe

Python gestionează memoria folosind semantica de numărare a referințelor. Odată ce un obiect nu mai este menționat, memoria lui este dealocată. Dar atâta timp cât există o referință, obiectul nu va fi dealocat. Lucruri precum referirile ciclice pot să te muște destul de greu.

Obiecte mici

CPython gestionează obiecte mici (mai puțin de 256 octeți) în bazine speciale cu limite de 8 octeți. Există piscine pentru 1-8 octeți, 9-16 octeți și până la 249-256 octeți. Atunci când este alocat un obiect de mărimea 10, acesta este alocat din bazinul de 16 octeți pentru obiecte de 9-16 octeți în mărime. Deci, chiar dacă conține doar 10 octeți de date, va costa 16 octeți de memorie. Dacă alocați 1.000.000 de obiecte de mărimea 10, de fapt, utilizați 16.000.000 de octeți și nu 10.000.000 de octeți, așa cum ați putea presupune. Această cheltuială de 60% nu este în mod evident banală.

Întregi

CPython păstrează o listă globală a tuturor numerelor întregi în intervalul [-5, 256]. Această strategie de optimizare are sens, deoarece numerele întregi se afișează peste tot, și având în vedere că fiecare număr întreg ia 24 de octeți, economisește o mulțime de memorie pentru un program tipic.

De asemenea, înseamnă că CPython pre-alocă 266 * 24 = 6384 octeți pentru toate aceste numere întregi, chiar dacă nu le folosiți pe cele mai multe dintre ele. Puteți să o verificați utilizând funcția id () care dă pointerul obiectului real. Dacă apelați id (x) multiplu pentru orice x în intervalul [-5, 256], veți obține același rezultat de fiecare dată (pentru același număr întreg). Dar dacă îl încercați pentru numere întregi din afara acestui interval, fiecare va fi diferit (un obiect nou este creat în zbor de fiecare dată).

Iată câteva exemple din acest domeniu:

"python id (-3) 140251817361752

id (-3) 140251817361752

id (-3) 140251817361752

id (201) 140251817366736

id (201) 140251817366736

id (201) 140251817366736 "

Iată câteva exemple în afara domeniului:

"python id (301) 140251846945800

id (301) 140251846945776

id (-6) 140251846946960

id (-6) 140251846946936 "

Memoria Python vs. memoria sistemului

CPython este un fel de posesiv. În multe cazuri, când obiectele de memorie din programul dvs. nu mai sunt menționate, acestea sunt nu returnate în sistem (de exemplu obiectele mici). Acest lucru este bun pentru programul dvs. dacă alocați și dezafectați multe obiecte (care aparțin aceleiași baze de 8 octeți), deoarece Python nu trebuie să deranjeze sistemul, ceea ce este relativ scump. Dar nu este chiar atât de minunat dacă programul dvs. utilizează în mod normal X octeți și sub o anumită condiție temporară folosește de 100 de ori mai mult (de exemplu, parsarea și procesarea unui fișier de configurare mare numai când începe).

Acum, acea memorie de 100X poate fi prinsă în mod inutil în programul tău, să nu mai fie folosită din nou și să refuzi sistemul să-l aloce altor programe. Ironia este că dacă utilizați modulul de procesare pentru a rula mai multe instanțe ale programului dvs., veți limita sever numărul de instanțe pe care le puteți rula pe o anumită mașină.

Memory Profiler

Pentru a măsura și măsura utilizarea reală a memoriei programului, puteți utiliza modulul memory_profiler. Am jucat cu ea puțin și nu sunt sigur că am încredere în rezultate. Folosirea este foarte simplă. Decorezi o funcție (poate fi funcția principală (0) cu decorator @profiler și atunci când programul iese, profilul de memorie tipărește la ieșirea standard un raport util care arată totalul și modificările memoriei pentru fiecare linie. program am fugit sub profiler:

"python din profilul de import memory_profiler

(i) în intervalul (100000): a.append (5) pentru i în intervalul (100000): b.append (300) pentru i în interval (100000): c.append ('123456789012345678901234567890') del a del b del c

imprimați "Efectuat!" dacă __name__ == '__main__': main () "

Aici este rezultatul:

Linia # Utilizare mem Utilizare incrementală Linie =========================================== ===== 3 22.9 MiB 0.0 MiB @profile 4 def principal (): 5 22.9 MiB 0.0 MiB a = [] 6 22.9 MiB 0.0 MiB b = [] 7 22.9 MiB 0.0 MiB c = [] 8 27.1 MiB 4.2 MiB pentru i în intervalul (100000): 9 27,1 MiB 0,0 MiB a.append (5) 10 27,5 MiB 0,4 MiB pentru i în intervalul (100000): 11 27,5 MiB 0,0 MiB b.append (300) 12 28,3 MiB 0,8 ​​MiB pentru i în intervalul (100000): 13 28.3 MiB 0.0 MiB c.append ('123456789012345678901234567890') 14 27.7 MiB -0.6 MiB del a 15 27.9 MiB 0.2 MiB del b 16 27.3 MiB -0.6 MiB del c 17 18 27.3 MiB 0.0 MiB print ' Terminat!' 

După cum puteți vedea, există 22,9 MB de memorie deasupra capului. Motivul pentru care memoria nu crește atunci când se adaugă numere întregi atât în ​​interiorul, cât și în afara intervalului [-5, 256] și, de asemenea, atunci când se adaugă șirul este că în toate cazurile se utilizează un singur obiect. Nu este clar de ce prima buclă din intervalul (100000) de pe linia 8 adaugă 4,2MB, în timp ce a doua pe linia 10 adaugă doar 0,4MB și a treia buclă pe linia 12 adaugă 0,8MB. În cele din urmă, atunci când ștergeți listele a, b și c, se eliberează 0,6 MB pentru a și c, dar pentru b se adaugă 0,2 MB. Nu pot înțelege din aceste rezultate.

Concluzie

CPython folosește o mulțime de memorie pentru obiectele sale. Utilizează diverse trucuri și optimizări pentru gestionarea memoriei. Urmărind utilizarea memoriei obiectului și conștientizarea modelului de gestionare a memoriei, puteți reduce semnificativ amprenta de memorie a programului dvs..

Aflați Python

Aflați Python cu ghidul nostru complet de instrucțiuni Python, indiferent dacă sunteți doar începători sau sunteți un coder experimentat în căutarea unor noi abilități.

Cod