Python Memory Management: The Essential Guide | Scout APM Blog Python Memory Management: The Essential Guide

Arwin Lashawn on December 04, 2020

Háttér

A Python nem arról ismert, hogy “gyors” programozási nyelv lenne. A Stack Overflow 2020-as fejlesztői felmérésének eredményei szerint azonban a Python a 2. legnépszerűbb programozási nyelv a JavaScript mögött (ahogy azt már sejthetted). Ez nagyrészt szuper barátságos szintaxisának és szinte bármilyen célra való alkalmazhatóságának köszönhető. Bár a Python nem a leggyorsabb nyelv, nagyszerű olvashatósága, párosulva a páratlan közösségi támogatással és a könyvtárak elérhetőségével, rendkívül vonzóvá tette a kóddal való munkához.

A Python memóriakezelése is szerepet játszik a népszerűségében. Hogyan? A Python memóriakezelése úgy van megvalósítva, hogy megkönnyíti az életünket. Hallottál már a Python memóriakezelőjéről? Ez a menedzser tartja kordában a Python memóriáját, így lehetővé teszi, hogy a kódodra koncentrálj ahelyett, hogy a memóriakezeléssel kellene foglalkoznod. Egyszerűsége miatt azonban a Python nem ad nagy szabadságot a memóriahasználat kezelésében, ellentétben az olyan nyelvekkel, mint a C++, ahol manuálisan lehet memóriát kiosztani és felszabadítani.

Azonban a Python memóriakezelés jó megértése egy nagyszerű kezdet, amely lehetővé teszi, hogy hatékonyabb kódot írj. Végső soron ezt olyan szokásként érvényesítheti, amelyet potenciálisan átvehet más programozási nyelveken is, amelyeket ismer.

Mit nyerünk tehát a memóriahatékony kód írásával?

  • Ez gyorsabb feldolgozást és kevesebb erőforrásigényt eredményez, nevezetesen a véletlen hozzáférésű memória (RAM) használatát. A több rendelkezésre álló RAM általában több helyet jelentene a gyorsítótár számára, ami segít felgyorsítani a lemezelérést. A memóriahatékony kód írásában az a nagyszerű, hogy nem feltétlenül kell több sornyi kódot írni.
  • Egy másik előnye, hogy megakadályozza a memóriaszivárgást, egy olyan problémát, amely a RAM-használat folyamatos növekedését okozza még a folyamatok leállítása esetén is, ami végül az eszköz teljesítményének lassulásához vagy romlásához vezet. Ezt az okozza, hogy a folyamatok befejezése után nem szabadítják fel a felhasznált memóriát.

A technikai világban talán hallotta már, hogy “a kész jobb, mint a tökéletes”. Tegyük fel azonban, hogy van két fejlesztő, akik Pythont használtak ugyanannak az alkalmazásnak a fejlesztéséhez, és ugyanannyi idő alatt fejezték be azt. Az egyikük memóriahatékonyabb kódot írt, ami gyorsabb teljesítményű alkalmazást eredményez. Inkább azt az alkalmazást választanád, amelyik simán fut, vagy azt, amelyik érezhetően lassabban fut? Ez egy jó példa arra, hogy két személy ugyanannyi időt töltene kódolással, mégis észrevehetően eltérő a kód teljesítménye.

Az alábbiakat tanulja meg az útmutatóban:

  • Hogyan történik a memóriakezelés a Pythonban?
  • Python szemétgyűjtés
  • Python memóriaprobléma felügyelete
  • Best Practices for Improving Python Code Performance

How is Memory Managed in Python?

A Python dokumentációja (3.9.0) szerint a Python memóriakezeléséhez egy privát halom tartozik, amelyet a program objektumainak és adatszerkezeteinek tárolására használnak. Ne feledje továbbá, hogy a Python memóriakezelője az, amely a memóriakezeléssel kapcsolatos piszkos munka nagy részét elvégzi, így Ön csak a kódjára koncentrálhat.

Python memóriaelosztás

A Pythonban minden egy objektum. Ahhoz, hogy ezek az objektumok hasznosak legyenek, a memóriában kell tárolni őket, hogy elérhetők legyenek. Mielőtt a memóriában tárolhatók lennének, először ki kell osztani vagy ki kell rendelni mindegyikhez egy darab memóriát.

A legalacsonyabb szinten a Python nyers memóriaallokátor először meggyőződik arról, hogy van-e szabad hely a privát halomban az objektumok tárolására. Ezt az operációs rendszer memóriakezelőjével együttműködve teszi. Tekintsük ezt úgy, mintha a Python programunk kérne az operációs rendszertől egy darab memóriát a munkához.

A következő szinten több objektum-specifikus allokátor működik ugyanazon a kupacon, és az objektum típusától függően különböző kezelési irányelveket hajt végre. Amint azt talán már tudja, néhány példa az objektumtípusokra a karakterláncok és az egész számok. Bár a karakterláncok és az egész számok talán nem is különböznek annyira, tekintve, hogy mennyi időnkbe telik felismerni és megjegyezni őket, a számítógépek mégis egészen másképp kezelik őket. Ennek az az oka, hogy a számítógépeknek más tárolási követelményekre és sebességkompromisszumokra van szükségük az egész számok esetében, mint a karakterláncok esetében.

Az utolsó dolog, amit a Python halom kezeléséről tudnod kell, hogy nulla irányításod van felette. Most talán elgondolkodik, hogy akkor hogyan írjunk memóriahatékony kódot, ha ilyen kevés kontrollunk van a Python memóriakezelése felett? Mielőtt erre rátérnénk, még jobban meg kell értenünk néhány fontos kifejezést a memóriakezeléssel kapcsolatban.

Statikus vs. dinamikus memóriaelosztás

Most, hogy megértettük, mi az a memóriaelosztás, itt az ideje, hogy megismerkedjünk a memóriaelosztás két típusával, nevezetesen a statikus és a dinamikus memóriaelosztással, és különbséget tegyünk a kettő között.

Statikus memóriaelosztás:

  • Amint azt a “statikus” szó is sugallja, a statikusan kiosztott változók állandóak, vagyis előzetesen ki kell őket osztani, és addig tartanak, amíg a program fut.
  • A memória kiosztása a fordítási idő alatt, vagyis a program végrehajtása előtt történik.
  • A verem adatstruktúra használatával történik, vagyis a változókat a verem memóriájában tároljuk.
  • A már kiosztott memória nem használható újra, tehát nincs memória újrafelhasználhatósága.

Dinamikus memória kiosztás:

  • Mint azt a “dinamikus” szó is sugallja, a dinamikusan kiosztott változók nem állandóak, és a program futása közben is kioszthatóak.
  • A memória kiosztása futási időben vagy a program végrehajtása közben történik.
  • A heap adatstruktúra használatával valósul meg, ami azt jelenti, hogy a változókat a heap memóriában tároljuk.
  • A kiosztott memória felszabadítható és újra felhasználható.

A dinamikus memória kiosztás egyik előnye a Pythonban, hogy nem kell előre aggódnunk, hogy mennyi memóriára van szükségünk a programunkhoz. Egy másik előnye, hogy az adatszerkezetek manipulációját szabadon végezhetjük anélkül, hogy aggódnunk kellene amiatt, hogy nagyobb memóriakiosztásra lesz szükség, ha az adatszerkezet bővül.

Mivel azonban a dinamikus memória kiosztása a program végrehajtása közben történik, ezért több időt vesz igénybe a program befejezése. Emellett a kiosztott memóriát a felhasználás után fel kell szabadítani. Ellenkező esetben potenciálisan olyan problémák léphetnek fel, mint például a memóriaszivárgás.

Fentebb kétféle memóriaszerkezettel találkoztunk: a heap-memóriával és a verem-memóriával. Vessünk rájuk egy mélyebb pillantást.

Stack memória

A stack memóriában tárolódnak a metódusok és változóik. Emlékszel, hogy a veremmemóriát a fordítási idő alatt osztjuk ki? Ez gyakorlatilag azt jelenti, hogy az ilyen típusú memóriához való hozzáférés nagyon gyors.

Amikor egy metódust meghívunk a Pythonban, egy veremkeret kerül kiosztásra. Ez a veremkeret fogja kezelni a metódus összes változóját. A metódus visszatérése után a veremkeret automatikusan megsemmisül.

Megjegyezzük, hogy a veremkeret felelős a metódus változóinak hatókörének beállításáért is.

Heap memória

Minden objektum és példányváltozó a heap memóriában tárolódik. Amikor egy változót létrehozunk a Pythonban, azt egy privát heap memóriában tároljuk, amely lehetővé teszi a ki- és visszahelyezést.

A heap memória lehetővé teszi, hogy ezeket a változókat a program összes metódusa globálisan elérje. A változó visszaadása után a Python szemétgyűjtője munkához lát, amelynek működésével később foglalkozunk.

Most nézzük meg a Python memóriaszerkezetét.

A Python memóriaszerkezetét tekintve három különböző szintje van:

  • Arenák
  • Poolok
  • Blokkok

Kezdjük a legnagyobbal, az arénákkal.

Arenák

Képzeljünk el egy íróasztalt, amelynek teljes felületét 64 könyv foglalja el. Az íróasztal teteje egy arénát képvisel, amelynek fix mérete 256KiB, amelyet a halomban osztunk ki (megjegyezzük, hogy a KiB különbözik a KB-tól, de a magyarázathoz feltételezhetjük, hogy ezek azonosak). Egy aréna a memória lehető legnagyobb darabját jelenti.

Közelebbről, az arénák olyan memória leképezések, amelyeket a Python allokátor, a pymalloc használ, amely kis objektumokra (512 bájtnál kisebb vagy azzal egyenlő) van optimalizálva. Az arénák felelősek a memória allokálásáért, ezért a későbbi struktúráknak ezt már nem kell elvégezniük.

Az aréna ezután tovább bontható 64 poolra, ami a következő legnagyobb memóriaszerkezet.

Poolok

Az asztali példához visszatérve, a könyvek egy arénán belül az összes poolt képviselik.

Minden pool jellemzően fix 4Kb méretű, és három lehetséges állapota lehet:

  • Üres: A pool üres, tehát kiosztható.
  • Használt: A pool olyan objektumokat tartalmaz, amelyek miatt nem üres és nem is tele.
  • Full: A pool megtelt, és így nem áll rendelkezésre további kiosztásra.

Megjegyezzük, hogy a pool méretének meg kell felelnie az operációs rendszer alapértelmezett memória lapméretének.

A pool ezután sok blokkra bontható, amelyek a legkisebb memóriastruktúrák.

Blokkok

Visszatérve az asztali példához, az egyes könyveken belüli oldalak a poolon belüli összes blokkot jelentik.

Az arénákkal és poolokkal ellentétben a blokkok mérete nem rögzített. Egy blokk mérete 8 és 512 bájt között mozog, és nyolcszorosának kell lennie.

Minden blokk csak egy bizonyos méretű Python-objektumot tárolhat, és három lehetséges állapota van:

  • Érintetlen: Nem lett kiosztva
  • Szabad: Ki lett osztva, de felszabadult és kiosztásra rendelkezésre állt
  • Kiosztva:

Megjegyezzük, hogy a memóriastruktúra három különböző szintje (arénák, poolok és blokkok), amelyeket fentebb tárgyaltunk, kifejezetten a kisebb Python-objektumok számára készült. A nagyobb objektumokat a Pythonon belül a szabványos C allokátorhoz irányítjuk, ami egy másik napra való olvasmány lenne.

Python szemétgyűjtés

A szemétgyűjtés egy olyan folyamat, amelyet egy program végez, hogy felszabadítsa a korábban kiosztott memóriát egy már nem használt objektum számára. A szemétgyűjtésre úgy is gondolhatunk, mint a memória újrahasznosítására vagy újrafelhasználására.

Régebben a programozóknak kézzel kellett ki- és felosztaniuk a memóriát. Ha elfelejtették a memória kiosztását megszüntetni, az memóriaszivárgáshoz vezetett, ami a végrehajtás teljesítményének csökkenéséhez vezetett. Ami még rosszabb, a kézi memória ki- és visszahelyezés még a memória véletlen felülírásához is vezethet, ami a program teljes összeomlásához vezethet.

A Pythonban a szemétgyűjtés automatikusan történik, ezért sok fejfájástól kíméli meg a memória ki- és visszahelyezés kézi kezelését. Konkrétan a Python a referenciaszámlálást generációs szemétgyűjtéssel kombinálva használja a nem használt memória felszabadítására. A hivatkozásszámlálás önmagában azért nem elegendő a Pythonban, mert nem tisztítja hatékonyan a lógó ciklikus hivatkozásokat.

A generációs szemétgyűjtési ciklus a következő lépéseket tartalmazza –

  1. A Python inicializál egy “selejtezési listát” a nem használt objektumok számára.
  2. Egy algoritmus fut le a referenciaciklusok felismerésére.
  3. Ha egy objektumból hiányoznak a külső hivatkozások, akkor az objektum a selejtezési listába kerül.
  4. Felszabadítja a memóriafoglalást a selejtezési listán lévő objektumok számára.

Ha többet szeretne megtudni a Pythonban a szemétgyűjtésről, akkor nézze meg a Python szemétgyűjtés című cikkünket: A Guide for Developers post.

Monitoring Python Memory Issues

Miözben mindenki szereti a Pythont, nem zárkózik el a memóriaproblémáktól. Ennek számos oka lehet.

A Python (3.9.0) memóriakezelési dokumentációja szerint a Python memóriakezelője nem feltétlenül adja vissza a memóriát az operációs rendszernek. A dokumentációban az áll, hogy “bizonyos körülmények között a Python memóriakezelő nem feltétlenül indít megfelelő műveleteket, például szemétgyűjtést, memóriatömörítést vagy más megelőző intézkedéseket.”

Ez azt eredményezi, hogy a Pythonban explicit módon kell felszabadítani a memóriát. Ennek egyik módja, hogy a Python szemétgyűjtőjét a gc modul használatával kényszeríthetjük a nem használt memória felszabadítására. Ehhez egyszerűen a gc.collect() parancsot kell futtatni. Ez azonban csak akkor nyújt észrevehető előnyöket, ha nagyon nagy számú objektumot manipulálunk.

A Python szemétgyűjtő időnként hibás működésén kívül, különösen nagy adathalmazok kezelése esetén, több Python könyvtárról is ismert, hogy memóriaszivárgást okoz. A Pandas például egy ilyen eszköz a radaron. Fontolja meg, hogy megnézi az összes memóriával kapcsolatos problémát a pandas hivatalos GitHub-tárában!

Az egyik nyilvánvaló ok, amely még a kódellenőrök éles szemei mellett is elsiklik, az, hogy a kódban nagyméretű objektumok maradnak, amelyeket nem adnak ki. Ugyanezen a ponton a végtelenül növekvő adatstruktúrák is aggodalomra adnak okot. Például egy növekvő szótár adatszerkezet fix méretkorlátozás nélkül.

A növekvő adatszerkezet megoldásának egyik módja, hogy a szótárat lehetőleg listává alakítjuk, és a listának beállítunk egy maximális méretet. Ellenkező esetben egyszerűen állítson be egy korlátot a szótár méretére, és törölje azt, amikor a korlátot eléri.

Most azon tűnődhet, hogyan lehet egyáltalán memóriaproblémákat észlelni? Az egyik lehetőség, hogy kihasználja az Application Performance Monitoring (APM) eszköz előnyeit. Emellett számos hasznos Python modul segíthet a memóriaproblémák követésében és nyomon követésében. Nézzük meg a lehetőségeinket, kezdve az APM eszközökkel.

Application Performance Monitoring (APM) Tools

Szóval, mi is pontosan az Application Performance Monitoring, és hogyan segít a memóriaproblémák felderítésében? Egy APM eszköz lehetővé teszi egy program valós idejű teljesítményméréseinek megfigyelését, lehetővé téve a folyamatos optimalizálást, amint felfedezi a teljesítményt korlátozó problémákat.

Az APM eszközök által generált jelentések alapján általános képet kaphat arról, hogyan teljesít a programja. Mivel valós idejű teljesítménymérési adatokat kaphat és figyelhet, azonnal intézkedhet az észlelt problémákra. Miután leszűkítette a program lehetséges területeit, amelyek a memóriaproblémák okozói lehetnek, belemerülhet a kódba, és megvitathatja azt a többi programozóval a javítandó kódsorok további meghatározása érdekében.

A memóriaszivárgási problémák gyökerének felkutatása önmagában is ijesztő feladat lehet. A javítás egy másik rémálom, mivel valóban meg kell értenie a kódját. Ha valaha is ilyen helyzetben találja magát, ne keressen tovább, mert a ScoutAPM egy hozzáértő APM eszköz, amely konstruktívan elemzi és optimalizálja az alkalmazás teljesítményét. A ScoutAPM valós idejű betekintést nyújt, így gyorsan megtalálhatja & megoldhatja a problémákat, mielőtt az ügyfelei észrevennék azokat.

Profilmodulok

Sok praktikus Python modul létezik, amelyekkel megoldhatja a memóriaproblémákat, legyen szó memóriaszivárgásról vagy a túlzott memóriahasználat miatt összeomló programjáról. Két ajánlott közülük:

  1. tracemalloc
  2. memory-profiler

Megjegyezzük, hogy csak a tracemalloc modul beépített, így mindenképpen telepítsd először a másik modult, ha használni szeretnéd.

tracemalloc

A Python (3.9.0) dokumentációja szerint a tracemalloc modul használatával a következő információkat kaphatjuk:

  • Traceback, ahol egy objektum kiosztásra került.
  • Statisztikák az allokált memóriablokkokról fájlnévenként és sorszámonként: az allokált memóriablokkok teljes mérete, száma és átlagos mérete.
  • Kalkulálja a két pillanatfelvétel közötti különbséget a memóriaszivárgások felderítéséhez.

A memóriaprobléma forrásának meghatározásához ajánlott első lépésként először a legtöbb memóriát allokáló fájlokat megjeleníteni. Ezt könnyen megteheti a dokumentációban bemutatott első kódpélda segítségével.

Ez azonban nem jelenti azt, hogy a kis mennyiségű memóriát allokáló fájlok nem fognak a végtelenségig növekedni, hogy a jövőben memóriaszivárgást okozzanak.

memory_profiler

Ez a modul egy szórakoztató modul. Dolgoztam vele, és személyes kedvencem, mert lehetőséget ad arra, hogy egyszerűen hozzáadjuk a @profile dekorátort bármelyik függvényhez, amelyet vizsgálni szeretnénk. Az eredményként kapott kimenet is nagyon könnyen érthető.

A másik ok, ami miatt ez a személyes kedvencem, hogy ez a modul lehetővé teszi az időalapú memóriahasználat grafikonjának ábrázolását. Néha egyszerűen csak egy gyors ellenőrzésre van szüksége, hogy lássa, hogy a memóriahasználat a végtelenségig tovább növekszik-e vagy sem. Erre ez a tökéletes megoldás, mivel nem kell soronkénti memóriaprofilozást végeznie a megerősítéshez. Egyszerűen csak megfigyelheti az ábrázolt grafikont, miután a profilozót egy bizonyos ideig hagyta futni. Íme egy példa a kimeneti grafikonra –

A memory-profiler dokumentációban található leírás szerint ez a Python modul egy folyamat memóriafogyasztásának megfigyelésére, valamint Python programok soronkénti elemzésére szolgál. Ez egy tisztán Python modul, amely a psutil könyvtártól függ.

A memory-profiler használatának további megismeréséhez ajánlom ennek a Medium blognak az elolvasását. Ott azt is megtudhatja, hogyan kell használni egy másik Python modult, a muppy-t (a legújabb a muppy3).

Best Practices for Improving Python Code Performance

Elég a memóriakezelés részleteiből. Most vizsgáljunk meg néhány jó szokást a memóriahatékony Python kód írásához.

A Python könyvtárak és beépített függvények kihasználása

Igen, ez egy olyan jó szokás, amelyet elég gyakran figyelmen kívül hagyhatunk. A Python páratlan közösségi támogatással rendelkezik, és ezt tükrözi a rengeteg Python-könyvtár, amelyek szinte bármilyen célra rendelkezésre állnak, az API-hívásoktól kezdve az adattudományig.

Ha létezik egy Python-könyvtár, amely lehetővé teszi, hogy ugyanazt a dolgot tegye, mint amit már megvalósított, akkor azt teheti, hogy összehasonlítja a kód teljesítményét, amikor a könyvtárat használja, mint amikor az egyéni kódját használja. Jó eséllyel a Python könyvtárak (különösen a népszerűek) memóriahatékonyabbak lesznek, mint a te kódod, mert a közösség visszajelzései alapján folyamatosan fejlesztik őket. Inkább olyan kódra támaszkodna, amelyet egyik napról a másikra készítettek, vagy olyanra, amelyet hosszú időn keresztül szigorúan fejlesztettek?

A legjobb az egészben, hogy a Python könyvtárak sok sor kódot megspórolnak Önnek, tehát miért ne tenné?

Nem használja a “+”-t a karakterláncok összekapcsolására

Mindannyian voltunk már bűnösek abban, hogy a “+” operátorral láncoltuk össze a karakterláncokat, mert olyan egyszerűnek tűnik.

Ne feledje, hogy a karakterláncok megváltoztathatatlanok. Ezért minden alkalommal, amikor a “+” operátorral hozzáadunk egy elemet egy karakterlánchoz, a Pythonnak egy új karakterláncot kell létrehoznia egy új memóriafoglalással. Hosszabb karakterláncok esetén a kód memóriahatékonysága egyre hangsúlyosabbá válik.

Itertools használata a hatékony ciklusoláshoz

A ciklusolás elengedhetetlen része a dolgok automatizálásának. Ahogy egyre többet és többet használjuk a ciklusokat, előbb-utóbb kénytelenek leszünk egymásba ágyazott ciklusokat használni, amelyek köztudottan nem hatékonyak a magas futásidejű komplexitásuk miatt.

Ez az a pont, ahol az itertools modul a segítségünkre siet. A Python itertools dokumentációja szerint: “A modul gyors, memóriahatékony eszközök alapvető készletét szabványosítja, amelyek önmagukban vagy kombinálva is hasznosak. Együtt lehetővé teszik, hogy speciális eszközöket tömören és hatékonyan építsünk fel tiszta Pythonban.”

Más szóval, az itertools modul lehetővé teszi a memóriahatékony hurkolást azáltal, hogy megszabadul a felesleges ciklusoktól. Érdekes módon az itertools modult azért nevezik gyöngyszemnek, mert lehetővé teszi elegáns megoldások összeállítását számtalan problémára.”

Nagyon biztos vagyok benne, hogy a következő kódodban legalább egy hurokkal fogsz dolgozni, úgyhogy akkor próbáld ki az itertools implementálását!

Összefoglalás és záró gondolatok

A jó Python memóriakezelési szokások alkalmazása nem az alkalmi programozóknak való. Ha általában egyszerű szkriptekkel boldogulsz, akkor egyáltalán nem kellene memóriával kapcsolatos problémákba ütköznöd. Hála a hardvereknek és szoftvereknek, amelyek e sorok olvasása közben is rohamos fejlődésen mennek keresztül, szinte minden létező eszköz alapmodellje, függetlenül a márkától, remekül futtathatja a mindennapi programokat. A memóriahatékony kód szükségessége csak akkor kezd megmutatkozni, amikor nagy kódbázison kezdünk dolgozni, különösen a termelésben, ahol a teljesítmény kulcsfontosságú.

Ez azonban nem jelenti azt, hogy a Pythonban a memóriakezelés nehezen felfogható fogalom lenne, és azt sem, hogy nem fontos. Ugyanis az alkalmazások teljesítményére napról napra nagyobb hangsúlyt fektetnek. Egy nap ez már nem csak puszta “kész” kérdés lesz. Ehelyett a fejlesztők azon fognak versenyezni, hogy olyan megoldást nyújtsanak, amely nemcsak az ügyfelek igényeit képes sikeresen megoldani, hanem mindezt villámgyorsan és minimális erőforrásokkal teszi.