- Background
- Cum este gestionată memoria în Python?
- Alocarea memoriei în Python
- Alocație de memorie statică vs. dinamică
- Memoria stivă
- Memoria heap
- Arene
- Pools
- Blocuri
- Python Garbage Collection
- Monitorizarea problemelor de memorie Python
- Instrumente de monitorizare a performanței aplicațiilor (APM)
- Module de profil
- tracemalloc
- memory_profiler
- Best Practices for Improving Python Code Performance
- Aprovizionați-vă de bibliotecile Python și de funcțiile încorporate
- Nu folosiți „+” pentru concatenarea șirurilor de caractere
- Utilizarea itertools pentru o buclă eficientă
- Recapitulare și gânduri de încheiere
Background
Python nu este cunoscut ca fiind un limbaj de programare „rapid”. Cu toate acestea, conform rezultatelor sondajului Stack Overflow Developer Survey din 2020, Python este al doilea cel mai popular limbaj de programare, după JavaScript (după cum probabil ați ghicit). Acest lucru se datorează în mare parte sintaxei sale super prietenoase și aplicabilității sale pentru aproape orice scop. Deși Python nu este cel mai rapid limbaj de pe piață, lizibilitatea sa excelentă, împreună cu suportul de neegalat al comunității și disponibilitatea bibliotecilor, l-au făcut extrem de atractiv pentru a face lucruri cu ajutorul codului.
Gestionarea memoriei din Python joacă un rol în popularitatea sa, de asemenea. Cum așa? Managementul memoriei Python este implementat într-un mod care ne face viața mai ușoară. Ați auzit vreodată de managerul de memorie Python? Este managerul care ține sub control memoria Python, permițându-vă astfel să vă concentrați asupra codului dvs. în loc să vă faceți griji cu privire la gestionarea memoriei. Cu toate acestea, datorită simplității sale, Python nu vă oferă prea multă libertate în gestionarea utilizării memoriei, spre deosebire de limbaje precum C++, unde puteți aloca și elibera manual memorie.
Cu toate acestea, a avea o bună înțelegere a gestionării memoriei Python este un bun început care vă va permite să scrieți cod mai eficient. În cele din urmă, îl puteți impune ca un obicei care poate fi potențial adoptat și în alte limbaje de programare pe care le cunoașteți.
Atunci, ce obținem din scrierea unui cod eficient din punct de vedere al memoriei?
- Conduce la o procesare mai rapidă și la o nevoie mai mică de resurse, și anume utilizarea memoriei cu acces aleatoriu (RAM). Mai multă memorie RAM disponibilă ar însemna, în general, mai mult spațiu pentru memoria cache, ceea ce va contribui la accelerarea accesului la disc. Lucrul grozav la scrierea unui cod care este eficient din punct de vedere al memoriei este că nu necesită neapărat să scrieți mai multe linii de cod.
- Un alt beneficiu este că previne scurgerile de memorie, o problemă care face ca utilizarea RAM să crească continuu chiar și atunci când procesele sunt ucise, ceea ce duce în cele din urmă la încetinirea sau afectarea performanțelor dispozitivului. Acest lucru este cauzat de eșecul de a elibera memoria utilizată după ce procesele se termină.
În lumea tehnologiei, este posibil să fi auzit că „făcut este mai bine decât perfect”. Cu toate acestea, să spunem că aveți doi dezvoltatori care au folosit Python în dezvoltarea aceleiași aplicații și că au finalizat-o în același interval de timp. Unul dintre ei a scris un cod mai eficient din punct de vedere al memoriei, ceea ce duce la o aplicație cu performanțe mai rapide. Ați prefera să alegeți aplicația care rulează fără probleme sau pe cea care rulează vizibil mai încet? Acesta este un bun exemplu în care doi indivizi ar petrece aceeași cantitate de timp codificând și totuși au performanțe ale codului vizibil diferite.
Iată ce veți învăța în ghid:
- Cum este gestionată memoria în Python?
- Python Garbage Collection
- Monitorizarea problemelor de memorie Python
- Cele mai bune practici pentru îmbunătățirea performanțelor codului Python
Cum este gestionată memoria în Python?
Conform documentației Python (3.9.0) pentru gestionarea memoriei, gestionarea memoriei Python implică un heap privat care este folosit pentru a stoca obiectele și structurile de date ale programului dumneavoastră. De asemenea, nu uitați că managerul de memorie Python este cel care se ocupă de cea mai mare parte a muncii murdare legate de gestionarea memoriei, astfel încât să vă puteți concentra doar asupra codului dumneavoastră.
Alocarea memoriei în Python
În Python totul este un obiect. Pentru ca aceste obiecte să fie utile, ele trebuie să fie stocate în memorie pentru a fi accesate. Înainte ca acestea să poată fi stocate în memorie, trebuie mai întâi alocată sau atribuită o bucată de memorie pentru fiecare dintre ele.
La cel mai mic nivel, alocatorul de memorie brută Python se va asigura mai întâi că există spațiu disponibil în heap-ul privat pentru a stoca aceste obiecte. El face acest lucru prin interacțiunea cu managerul de memorie al sistemului de operare. Priviți-l ca pe un program Python care solicită sistemului de operare o bucată de memorie cu care să lucreze.
La nivelul următor, mai mulți alocatori specifici pentru obiecte operează pe același heap și implementează politici de gestionare distincte în funcție de tipul de obiect. După cum probabil știți deja, câteva exemple de tipuri de obiecte sunt șirurile de caractere și numerele întregi. Deși șirurile de caractere și numerele întregi pot să nu fie atât de diferite având în vedere cât timp ne ia să le recunoaștem și să le memorăm, ele sunt tratate foarte diferit de către calculatoare. Acest lucru se datorează faptului că calculatoarele au nevoie de cerințe de stocare și compromisuri de viteză diferite pentru întregi în comparație cu șirurile de caractere.
Un ultim lucru pe care ar trebui să-l știți despre modul în care este gestionat heap-ul Python este că nu aveți niciun control asupra lui. Acum s-ar putea să vă întrebați, cum scriem atunci cod eficient din punct de vedere al memoriei dacă avem atât de puțin control asupra gestionării memoriei Python? Înainte de a intra în acest subiect, trebuie să înțelegem mai bine câțiva termeni importanți privind gestionarea memoriei.
Alocație de memorie statică vs. dinamică
Acum că ați înțeles ce este alocarea memoriei, este timpul să vă familiarizați cu cele două tipuri de alocare a memoriei, și anume statică și dinamică, și să faceți distincția între cele două.
Alocarea statică a memoriei:
- După cum sugerează cuvântul „static”, variabilele alocate static sunt permanente, ceea ce înseamnă că trebuie alocate în prealabil și durează atâta timp cât programul rulează.
- Memoria este alocată în timpul compilării sau înainte de execuția programului.
- Implementată folosind structura de date a stivei, ceea ce înseamnă că variabilele sunt stocate în memoria stivei.
- Memoria care a fost alocată nu poate fi reutilizată, deci nu există posibilitatea de reutilizare a memoriei.
Alocarea dinamică a memoriei:
- După cum sugerează cuvântul „dinamic”, variabilele alocate dinamic nu sunt permanente și pot fi alocate pe măsură ce un program rulează.
- Memoria este alocată în timpul rulării sau în timpul execuției programului.
- Implementat folosind structura de date heap, ceea ce înseamnă că variabilele sunt stocate în memoria heap.
- Memoria care a fost alocată poate fi eliberată și reutilizată.
Un avantaj al alocării dinamice a memoriei în Python este că nu trebuie să ne îngrijorăm în prealabil de câtă memorie avem nevoie pentru programul nostru. Un alt avantaj este că manipularea structurilor de date se poate face liber, fără a trebui să ne facem griji cu privire la necesitatea unei alocări mai mari de memorie dacă structura de date se extinde.
Cu toate acestea, deoarece alocarea dinamică a memoriei se face în timpul execuției programului, aceasta va consuma mai mult timp pentru finalizarea sa. De asemenea, memoria care a fost alocată trebuie să fie eliberată după ce a fost utilizată. În caz contrar, pot apărea potențial probleme precum scurgerile de memorie.
Am întâlnit mai sus două tipuri de structuri de memorie – memoria heap și memoria stivă. Să le analizăm mai în profunzime.
Memoria stivă
Toate metodele și variabilele lor sunt stocate în memoria stivă. Vă amintiți că memoria stivă este alocată în timpul compilării? Acest lucru înseamnă efectiv că accesul la acest tip de memorie este foarte rapid.
Când o metodă este apelată în Python, se alocă un cadru de stivă. Acest cadru de stivă va gestiona toate variabilele metodei. După ce metoda este returnată, cadrul stivei este distrus automat.
Rețineți că cadrul stivei este, de asemenea, responsabil pentru stabilirea domeniului de cuprindere pentru variabilele unei metode.
Memoria heap
Toate obiectele și variabilele de instanță sunt stocate în memoria heap. Atunci când o variabilă este creată în Python, aceasta este stocată într-un heap privat care va permite apoi alocarea și dezalocarea.
Memoria heap permite ca aceste variabile să fie accesate la nivel global de către toate metodele programului dumneavoastră. După ce variabila este returnată, colectorul de gunoi Python se pune la treabă, a cărui funcționare o vom aborda mai târziu.
Acum să aruncăm o privire la structura de memorie Python.
Python are trei niveluri diferite când vine vorba de structura sa de memorie:
- Arene
- Poluri
- Blocuri
Vom începe cu cel mai mare dintre toate – arenele.
Arene
Imaginați-vă un birou cu 64 de cărți care acoperă întreaga sa suprafață. Partea de sus a biroului reprezintă o arenă care are o dimensiune fixă de 256KiB care este alocată în heap (Rețineți că KiB este diferit de KB, dar puteți presupune că sunt identice pentru această explicație). O arenă reprezintă cea mai mare bucată de memorie posibilă.
Mai precis, arenele sunt mape de memorie care sunt folosite de alocatorul Python, pymalloc, care este optimizat pentru obiecte mici (mai mici sau egale cu 512 octeți). Arenele sunt responsabile de alocarea memoriei și, prin urmare, structurile ulterioare nu mai trebuie să facă acest lucru.
Acestă arenă poate fi apoi împărțită în 64 de pool-uri, care este următoarea structură de memorie cea mai mare.
Pools
Întorcându-ne la exemplul biroului, cărțile reprezintă toate pool-urile din cadrul unei arene.
Care pool ar avea de obicei o dimensiune fixă de 4Kb și poate avea trei stări posibile:
- Gol: Pool-ul este gol și, prin urmare, disponibil pentru alocare.
- Utilizat: Pool-ul conține obiecte care fac ca acesta să nu fie nici gol, nici plin.
- Full: Pool-ul este plin și, prin urmare, nu mai este disponibil pentru alocare.
Rețineți că dimensiunea pool-ului ar trebui să corespundă dimensiunii implicite a paginilor de memorie ale sistemului de operare.
Un pool este apoi împărțit în mai multe blocuri, care sunt cele mai mici structuri de memorie.
Blocuri
Returnându-ne la exemplul biroului, paginile din cadrul fiecărei cărți reprezintă toate blocurile din cadrul unui pool.
Dincolo de arene și pool-uri, dimensiunea unui bloc nu este fixă. Dimensiunea unui bloc variază între 8 și 512 octeți și trebuie să fie un multiplu de opt.
Care bloc poate stoca doar un obiect Python de o anumită dimensiune și are trei stări posibile:
- Neatins: Nu a fost alocat
- Liber: A fost alocat, dar a fost eliberat și pus la dispoziție pentru alocare
- Alocat: A fost alocat
Rețineți că cele trei niveluri diferite ale unei structuri de memorie (arene, pool-uri și blocuri), despre care am discutat mai sus, sunt destinate în special obiectelor Python mai mici. Obiectele mari sunt direcționate către alocatorul standard C din Python, care ar fi o lectură bună pentru o altă zi.
Python Garbage Collection
Garbage collection este un proces efectuat de un program pentru a elibera memoria alocată anterior pentru un obiect care nu mai este utilizat. Vă puteți gândi la alocarea gunoiului ca la reciclarea sau reutilizarea memoriei.
În trecut, programatorii trebuiau să aloce și să dezaloce manual memoria. Uitarea dezalocării memoriei ar fi condus la o scurgere de memorie, ceea ce ar fi dus la o scădere a performanțelor de execuție. Mai rău, alocarea și dezalocarea manuală a memoriei sunt chiar susceptibile să ducă la suprascrierea accidentală a memoriei, ceea ce poate duce la blocarea completă a programului.
În Python, colectarea gunoiului se face automat și, prin urmare, vă scutește de multe bătăi de cap pentru a gestiona manual alocarea și dezalocarea memoriei. În mod specific, Python utilizează numărarea referințelor combinată cu colectarea de gunoi generațională pentru a elibera memoria neutilizată. Motivul pentru care doar numărarea referințelor nu este suficientă pentru Python este că nu curăță eficient referințele ciclice suspendate.
Un ciclu generațional de colectare a gunoiului conține următorii pași –
- Python inițializează o „listă de eliminare” pentru obiectele nefolosite.
- Se execută un algoritm pentru a detecta ciclurile de referințe.
- Dacă unui obiect îi lipsesc referințele exterioare, acesta este introdus în lista de aruncare.
- Se eliberează alocarea de memorie pentru obiectele din lista de respingere.
Pentru a afla mai multe despre colectarea gunoiului în Python, puteți consulta pagina noastră Python Garbage Collection: A Guide for Developers (Un ghid pentru dezvoltatori).
Monitorizarea problemelor de memorie Python
Chiar dacă toată lumea iubește Python, acesta nu se ferește să aibă probleme de memorie. Există multe motive posibile.
Conform documentației Python (3.9.0) pentru gestionarea memoriei, managerul de memorie Python nu eliberează neapărat memoria înapoi către sistemul de operare. Se afirmă în documentație că „în anumite circumstanțe, este posibil ca managerul de memorie Python să nu declanșeze acțiuni adecvate, cum ar fi colectarea gunoiului, compactarea memoriei sau alte măsuri preventive.”
Ca urmare, este posibil să fie nevoie să se elibereze explicit memoria în Python. O modalitate de a face acest lucru este de a forța colectorul de gunoi Python să elibereze memoria nefolosită prin utilizarea modulului gc. Pentru a face acest lucru este suficient să se ruleze gc.collect(). Cu toate acestea, acest lucru oferă beneficii notabile doar atunci când se manipulează un număr foarte mare de obiecte.
Pe lângă natura ocazional eronată a colectorului de gunoi Python, în special atunci când se lucrează cu seturi mari de date, mai multe biblioteci Python sunt, de asemenea, cunoscute pentru a provoca scurgeri de memorie. Pandas, de exemplu, este un astfel de instrument aflat în vizor. Luați în considerare posibilitatea de a arunca o privire la toate problemele legate de memorie din depozitul oficial Pandas de pe GitHub!
Un motiv evident care poate scăpa chiar și ochilor ageri ai revizorilor de cod este faptul că există obiecte mari persistente în cod care nu sunt eliberate. În aceeași notă, structurile de date care cresc la infinit reprezintă un alt motiv de îngrijorare. De exemplu, o structură de date de tip dicționar în creștere fără o limită de dimensiune fixă.
O modalitate de a rezolva structura de date în creștere este de a converti dicționarul într-o listă, dacă este posibil, și de a stabili o dimensiune maximă pentru listă. În caz contrar, setați pur și simplu o limită pentru dimensiunea dicționarului și ștergeți-o ori de câte ori este atinsă limita.
Acum poate vă întrebați, cum detectez problemele de memorie în primul rând? O opțiune este să profitați de un instrument de monitorizare a performanței aplicațiilor (APM). În plus, multe module Python utile vă pot ajuta să urmăriți și să localizați problemele de memorie. Să ne uităm la opțiunile noastre, începând cu instrumentele APM.
Instrumente de monitorizare a performanței aplicațiilor (APM)
Deci, ce este mai exact Monitorizarea performanței aplicațiilor și cum ajută la depistarea problemelor de memorie? Un instrument APM vă permite să observați în timp real parametrii de performanță ai unui program, permițând o optimizare continuă pe măsură ce descoperiți problemele care limitează performanța.
Pe baza rapoartelor generate de instrumentele APM, veți putea avea o idee generală despre modul în care funcționează programul dumneavoastră. Deoarece puteți primi și monitoriza în timp real măsurătorile de performanță, puteți lua măsuri imediate în legătură cu orice problemă observată. Odată ce ați restrâns posibilele zone din programul dumneavoastră care pot fi vinovate pentru problemele de memorie, puteți apoi să vă scufundați în cod și să discutați cu ceilalți colaboratori de cod pentru a determina mai departe liniile specifice de cod care trebuie reparate.
Descoperirea rădăcinii problemelor de pierderi de memorie poate fi o sarcină descurajantă. Corectarea acesteia este un alt coșmar, deoarece trebuie să vă înțelegeți cu adevărat codul. Dacă vă aflați vreodată în această poziție, nu mai căutați, deoarece ScoutAPM este un instrument APM competent care poate analiza și optimiza constructiv performanța aplicației dumneavoastră. ScoutAPM vă oferă o perspectivă în timp real, astfel încât să puteți identifica rapid & să rezolvați problemele înainte ca clienții dvs. să le detecteze.
Module de profil
Există multe module Python la îndemână pe care le puteți folosi pentru a rezolva problemele de memorie, fie că este vorba de o scurgere de memorie, fie că programul dvs. se blochează din cauza utilizării excesive a memoriei. Două dintre cele recomandate sunt:
- tracemalloc
- memory-profiler
Rețineți că doar modulul tracemalloc este încorporat, așa că asigurați-vă că instalați mai întâi celălalt modul dacă doriți să îl utilizați.
tracemalloc
Potrivit documentației Python (3.9.0) pentru tracemalloc, utilizarea acestui modul vă poate oferi următoarele informații:
- Traceback în cazul în care un obiect a fost alocat.
- Statistici privind blocurile de memorie alocate pe nume de fișier și pe număr de linie: dimensiunea totală, numărul și dimensiunea medie a blocurilor de memorie alocate.
- Calculează diferența dintre două instantanee pentru a detecta scurgerile de memorie.
Un prim pas recomandat pe care ar trebui să-l faceți pentru a determina sursa problemei de memorie este să afișați mai întâi fișierele care alocă cea mai multă memorie. Puteți face acest lucru cu ușurință folosind primul exemplu de cod prezentat în documentație.
Aceasta nu înseamnă însă că fișierele care alocă o cantitate mică de memorie nu vor crește la nesfârșit pentru a provoca scurgeri de memorie în viitor.
memory_profiler
Acest modul este unul distractiv. Am lucrat cu acesta și este unul dintre preferatele mele personale, deoarece oferă opțiunea de a adăuga pur și simplu decoratorul @profile la orice funcție pe care doriți să o investigați. Rezultatul oferit ca rezultat este, de asemenea, foarte ușor de înțeles.
Un alt motiv care îl face să fie preferatul meu personal este că acest modul vă permite să trasați un grafic al utilizării memoriei în funcție de timp. Uneori, aveți pur și simplu nevoie de o verificare rapidă pentru a vedea dacă utilizarea memoriei continuă să crească la nesfârșit sau nu. Aceasta este soluția perfectă pentru acest lucru, deoarece nu este nevoie să faceți o profilare a memoriei linie cu linie pentru a confirma acest lucru. Puteți observa pur și simplu graficul trasat după ce lăsați profilerul să ruleze pentru o anumită durată. Iată un exemplu de grafic ieșit –
Conform descrierii din documentația memory-profiler, acest modul Python este destinat monitorizării consumului de memorie al unui proces, precum și unei analize linie cu linie a acestuia pentru programele Python. Este un modul Python pur care depinde de biblioteca psutil.
Vă recomand să citiți acest blog Medium pentru a explora în continuare modul în care este utilizat memory-profiler. Acolo veți învăța, de asemenea, cum să utilizați un alt modul Python, muppy (cel mai recent este muppy3).
Best Practices for Improving Python Code Performance
Suficient cu toate detaliile despre managementul memoriei. Acum haideți să explorăm câteva dintre bunele obiceiuri în scrierea unui cod Python eficient din punct de vedere al memoriei.
Aprovizionați-vă de bibliotecile Python și de funcțiile încorporate
Da, acesta este un bun obicei care poate fi destul de des trecut cu vederea. Python are un sprijin de neegalat din partea comunității și acest lucru este reflectat de abundența bibliotecilor Python disponibile pentru aproape orice scop, de la apeluri API la știința datelor.
Dacă există o bibliotecă Python care vă permite să faceți același lucru ca și ceea ce ați implementat deja, ceea ce puteți face este să comparați performanța codului dvs. atunci când folosiți biblioteca în comparație cu atunci când folosiți codul dvs. personalizat. Există șanse ca bibliotecile Python (în special cele populare) să fie mai eficiente din punct de vedere al memoriei decât codul dumneavoastră, deoarece sunt îmbunătățite continuu pe baza feedback-ului comunității. Ați prefera să vă bazați pe un cod care a fost creat peste noapte sau pe unul care a fost îmbunătățit riguros pentru o perioadă îndelungată?
Cel mai bun lucru este că bibliotecile Python vă vor economisi multe linii de cod, așa că de ce nu?
Nu folosiți „+” pentru concatenarea șirurilor de caractere
La un moment dat, cu toții am fost vinovați de concatenarea șirurilor de caractere folosind operatorul „+” pentru că pare atât de ușor.
Rețineți că șirurile de caractere sunt imuabile. Prin urmare, de fiecare dată când adăugați un element la un șir de caractere cu operatorul „+”, Python trebuie să creeze un nou șir de caractere cu o nouă alocare de memorie. Cu șiruri mai lungi, ineficiența de memorie a codului va deveni mai pronunțată.
Utilizarea itertools pentru o buclă eficientă
Buclajul este o parte esențială a automatizării lucrurilor. Pe măsură ce continuăm să folosim buclele din ce în ce mai mult, ne vom trezi în cele din urmă că trebuie să folosim bucle imbricate, despre care se știe că sunt ineficiente din cauza complexității lor ridicate în timp de execuție.
Acesta este momentul în care modulul itertools vine în ajutor. Conform documentației itertools din Python, „Modulul standardizează un set de bază de instrumente rapide și eficiente din punct de vedere al memoriei, care sunt utile singure sau în combinație. Împreună, acestea fac posibilă construirea de instrumente specializate în mod succint și eficient în Python pur.”
Cu alte cuvinte, modulul itertools permite realizarea de bucle eficiente din punct de vedere al memoriei prin eliminarea buclelor inutile. Interesant, modulul itertools este numit o bijuterie deoarece permite compunerea unor soluții elegante pentru o multitudine de probleme.
Sunt destul de sigur că veți lucra cu cel puțin o buclă în următoarea bucată de cod, așa că încercați să implementați itertools atunci!
Recapitulare și gânduri de încheiere
Aplicarea unor bune obiceiuri de gestionare a memoriei în Python nu este pentru programatorul ocazional. Dacă de obicei vă descurcați cu scripturi simple, nu ar trebui să vă confruntați deloc cu probleme legate de memorie. Mulțumită hardware-ului și software-ului care continuă să cunoască progrese rapide în timp ce citiți aceste rânduri, modelul de bază al aproape oricărui dispozitiv existent, indiferent de mărcile lor, ar trebui să ruleze foarte bine programele de zi cu zi. Necesitatea unui cod eficient din punct de vedere al memoriei începe să se manifeste doar atunci când începeți să lucrați la o bază de cod mare, în special pentru producție, unde performanța este esențială.
Cu toate acestea, acest lucru nu sugerează că gestionarea memoriei în Python este un concept dificil de înțeles și nici nu înseamnă că nu este important. Acest lucru se datorează faptului că accentul pus pe performanța aplicațiilor crește în fiecare zi. Într-o zi, nu va mai fi doar o simplă chestiune de „făcut”. În schimb, dezvoltatorii vor fi în competiție pentru a livra o soluție care nu numai că este capabilă să rezolve cu succes nevoile clienților, dar o face cu o viteză fulminantă și cu resurse minime.
.