Správa paměti Pythonu: Základní průvodce

Arwin Lashawn dne 04. prosince 2020

Pozadí

Python není známý jako „rychlý“ programovací jazyk. Podle výsledků průzkumu Stack Overflow Developer Survey 2020 je však Python druhým nejoblíbenějším programovacím jazykem hned za JavaScriptem (jak jste možná uhodli). Je to dáno především jeho super přívětivou syntaxí a použitelností pro téměř jakýkoli účel. Python sice není nejrychlejší jazyk, ale jeho skvělá čitelnost spolu s bezkonkurenční podporou komunity a dostupností knihoven z něj udělaly mimořádně atraktivní jazyk pro práci s kódem.

Rozhodující roli v jeho popularitě hraje také správa paměti v Pythonu. Jak to? Správa paměti v jazyce Python je implementována způsobem, který nám usnadňuje život. Slyšeli jste někdy o správci paměti v jazyce Python? Je to správce, který udržuje paměť Pythonu pod kontrolou, a umožňuje vám tak soustředit se na váš kód, místo abyste se museli starat o správu paměti. Vzhledem ke své jednoduchosti vám však Python neposkytuje velkou volnost při správě využití paměti, na rozdíl od jazyků, jako je C++, kde můžete ručně alokovat a uvolňovat paměť.

Dobrá znalost správy paměti v jazyce Python je však skvělým začátkem, který vám umožní psát efektivnější kód. Nakonec si jej můžete prosadit jako zvyk, který lze případně převzít i v jiných programovacích jazycích, které znáte.

Co tedy získáme psaním kódu s efektivním využitím paměti?

  • Vede to k rychlejšímu zpracování a menší potřebě zdrojů, konkrétně využití paměti RAM (random access memory). Více dostupné paměti RAM by obecně znamenalo více místa pro mezipaměť, což pomůže zrychlit přístup k disku. Skvělé na psaní kódu, který je paměťově úsporný, je to, že nemusí nutně vyžadovat psaní více řádků kódu.
  • Další výhodou je, že zabraňuje úniku paměti, což je problém, který způsobuje neustálé zvyšování využití paměti RAM i po ukončení procesů, což nakonec vede ke zpomalení nebo zhoršení výkonu zařízení. Příčinou je neuvolnění použité paměti po ukončení procesů.

V technickém světě jste možná slyšeli, že „hotovo je lepší než dokonalé“. Řekněme však, že máte dva vývojáře, kteří při vývoji stejné aplikace použili Python a dokončili ji ve stejném čase. Jeden z nich napsal paměťově úspornější kód, jehož výsledkem je rychleji fungující aplikace. Vybrali byste si raději aplikaci, která běží plynule, nebo tu, která běží znatelně pomaleji? To je jeden z dobrých příkladů, kdy by dva jedinci strávili kódováním stejné množství času, a přesto by měli znatelně odlišný výkon kódu.

Tady se dozvíte, co se v příručce dozvíte:

  • Jak se v jazyce Python spravuje paměť?
  • Python Garbage Collection
  • Monitorování problematiky paměti v Pythonu
  • Nejlepší postupy pro zlepšení výkonu kódu v Pythonu

Jak se v Pythonu spravuje paměť?“

Podle dokumentace Pythonu (3.9.0) pro správu paměti zahrnuje správa paměti v Pythonu soukromou haldu, která se používá k ukládání objektů a datových struktur vašeho programu. Nezapomeňte také, že právě správce paměti Pythonu obstarává většinu špinavé práce související se správou paměti, takže se můžete soustředit pouze na svůj kód.

Alokace paměti v Pythonu

Všechno v Pythonu je objekt. Aby byly tyto objekty užitečné, musí být uloženy v paměti, aby k nim bylo možné přistupovat. Než mohou být uloženy do paměti, musí být pro každý z nich nejprve alokován nebo přiřazen kus paměti.

Na nejnižší úrovni se alokátor hrubé paměti jazyka Python nejprve ujistí, že je v soukromé haldě volné místo pro uložení těchto objektů. To provede interakcí se správcem paměti operačního systému. Podívejte se na to tak, že váš program Python požádá operační systém o kus paměti, se kterým by mohl pracovat.

Na další úrovni pracuje na stejné haldě několik objektově specifických alokátorů, které implementují odlišné zásady správy v závislosti na typu objektu. Jak již možná víte, příkladem objektových typů jsou řetězce a celá čísla. Přestože se řetězce a celá čísla možná až tak neliší vzhledem k tomu, kolik času nám zabere jejich rozpoznání a zapamatování, počítače s nimi zacházejí velmi odlišně. Je to proto, že počítače potřebují jiné požadavky na úložiště a kompromisy v rychlosti pro celá čísla ve srovnání s řetězci.

Jedna z posledních věcí, kterou byste měli vědět o tom, jak je v Pythonu spravována halda, je, že nad ní máte nulovou kontrolu. Možná si teď říkáte, jak tedy máme psát paměťově úsporný kód, když máme nad správou paměti v jazyce Python tak malou kontrolu? Než se k tomu dostaneme, musíme dále porozumět některým důležitým pojmům týkajícím se správy paměti.

Statické vs. dynamické přidělování paměti

Teď, když jste pochopili, co je to přidělování paměti, je čas seznámit se se dvěma typy přidělování paměti, a to statickým a dynamickým, a rozlišovat mezi nimi.

Statická alokace paměti:

  • Podobně jako slovo „statická“ napovídá, že staticky alokované proměnné jsou trvalé, což znamená, že musí být alokovány předem a trvají tak dlouho, dokud program běží.
  • Paměť je alokována během kompilace nebo před spuštěním programu.
  • Provádí se pomocí datové struktury zásobníku, což znamená, že proměnné jsou uloženy v paměti zásobníku.
  • Paměť, která byla alokována, nemůže být použita opakovaně, tedy neexistuje možnost opakovaného použití paměti.

Dynamická alokace paměti:

  • Jak napovídá slovo „dynamická“, dynamicky alokované proměnné nejsou trvalé a mohou být alokovány za běhu programu.
  • Paměť je alokována za běhu nebo během provádění programu.
  • Provádí se pomocí datové struktury haldy, což znamená, že proměnné jsou uloženy v paměti haldy.
  • Paměť, která byla alokována, může být uvolněna a znovu použita.

Jednou z výhod dynamického přidělování paměti v jazyce Python je, že se nemusíme předem starat o to, kolik paměti pro náš program potřebujeme. Další výhodou je, že manipulaci s datovou strukturou můžeme provádět volně, aniž bychom se museli obávat potřeby vyšší alokace paměti v případě, že se datová struktura rozšíří.

Jelikož se však dynamická alokace paměti provádí během provádění programu, spotřebuje více času na jeho dokončení. Také alokovanou paměť je třeba po jejím využití uvolnit. V opačném případě může potenciálně dojít k problémům, jako je únik paměti.

Výše jsme se setkali se dvěma typy paměťových struktur – pamětí na hromadě a pamětí na zásobníku. Podívejme se na ně hlouběji.

Paměť zásobníku

V paměti zásobníku jsou uloženy všechny metody a jejich proměnné. Pamatujete si, že paměť zásobníku je alokována během kompilace? To fakticky znamená, že přístup k tomuto typu paměti je velmi rychlý.

Při volání metody v jazyce Python je alokován rámec zásobníku. Tento zásobníkový rámec bude zpracovávat všechny proměnné dané metody. Po návratu metody je zásobníkový rámec automaticky zničen.

Všimněte si, že zásobníkový rámec je také zodpovědný za nastavení rozsahu proměnných metody.

Paměť haldy

Všechny objekty a proměnné instance jsou uloženy v paměti haldy. Když je v Pythonu vytvořena proměnná, je uložena v soukromé haldě, která pak umožní její alokaci a dealokalizaci.

Paměť haldy umožňuje, aby k těmto proměnným měly globální přístup všechny metody vašeho programu. Po vrácení proměnné se do práce pustí Python garbage collector, jehož fungování se budeme věnovat později.

Nyní se podíváme na strukturu paměti jazyka Python.

Python má tři různé úrovně, pokud jde o strukturu paměti:

  • Arény
  • Poly
  • Bloky

Začneme největší z nich – arénami.

Arény

Představte si stůl s 64 knihami pokrývajícími celou jeho plochu. Horní část stolu představuje jednu arénu, která má pevnou velikost 256KiB, jež je alokována v haldě (všimněte si, že KiB se liší od KB, ale pro tento výklad můžete předpokládat, že jsou stejné). Aréna představuje největší možný kus paměti.

Přesněji řečeno, arény jsou mapování paměti, které používá alokátor Pythonu, pymalloc, který je optimalizován pro malé objekty (menší nebo rovné 512 bajtům). Arény jsou zodpovědné za alokaci paměti, a proto to následné struktury již nemusí dělat.

Tuto arénu pak lze dále rozdělit na 64 poolů, což je další největší paměťová struktura.

Pools

Pokud se vrátíme k příkladu se stolem, knihy představují všechny pooly v rámci jedné arény.

Každý pool bude mít obvykle pevnou velikost 4Kb a může mít tři možné stavy:

  • Prázdný: Bazén je prázdný a je tedy k dispozici pro alokaci.
  • Používá se: Fond obsahuje objekty, kvůli kterým není ani prázdný, ani plný.
  • Plný: Fond je plný, a není tedy k dispozici pro další alokaci.

Všimněte si, že velikost fondu by měla odpovídat výchozí velikosti stránky paměti vašeho operačního systému.

Fond je pak rozdělen na mnoho bloků, což jsou nejmenší paměťové struktury.

Bloky

Pokud se vrátíme k příkladu se stolem, stránky v každé knize představují všechny bloky v rámci fondu.

Na rozdíl od arén a fondů není velikost bloku pevně stanovena. Velikost bloku se pohybuje od 8 do 512 bajtů a musí být násobkem osmi.

Každý blok může uchovávat pouze jeden objekt Pythonu určité velikosti a má tři možné stavy:

  • Nedotčený: Nebyl alokován
  • Volný: Byl alokován, ale byl uvolněn a dán k dispozici pro alokaci
  • Alokován:

Všimněte si, že tři různé úrovně paměťové struktury (arény, pooly a bloky), o kterých jsme hovořili výše, jsou určeny speciálně pro menší objekty jazyka Python. Velké objekty jsou v rámci Pythonu nasměrovány na standardní alokátor jazyka C, což by bylo dobré čtení na jindy.

Python Garbage Collection

Garbage collection je proces prováděný programem za účelem uvolnění dříve alokované paměti pro objekt, který již není používán. Alokaci odpadu si můžete představit jako recyklaci nebo opětovné využití paměti.

Dříve museli programátoři paměť alokovat a dealokovat ručně. Zapomenutí na dealokování paměti by vedlo k úniku paměti, což by vedlo k poklesu výkonu provádění. A co hůř, ruční alokace a dealokalizace paměti může dokonce vést k náhodnému přepsání paměti, což může způsobit úplný pád programu.

V jazyce Python se garbage collection provádí automaticky, a proto vám ušetří spoustu starostí s ruční správou alokace a dealokalizace paměti. Konkrétně Python k uvolnění nepoužívané paměti používá počítání referencí v kombinaci s generačním garbage collection. Důvod, proč samotné počítání referencí pro Python nestačí, je ten, že neumí efektivně vyčistit visící cyklické reference.

Generační cyklus garbage collection obsahuje následující kroky –

  1. Python inicializuje „discard list“ pro nepoužívané objekty.
  2. Je spuštěn algoritmus pro detekci referenčních cyklů.
  3. Pokud objekt postrádá vnější reference, je vložen do seznamu vyřazených objektů.
  4. Uvolní se alokace paměti pro objekty v seznamu vyřazených objektů.

Chcete-li se o garbage collection v jazyce Python dozvědět více, můžete se podívat na náš článek Python Garbage Collection:

Monitorování problémů s pamětí v Pythonu

Přestože má Python každý rád, nevyhýbá se problémům s pamětí. Možných důvodů je mnoho.

Podle dokumentace k jazyku Python (3.9.0) pro správu paměti nemusí správce paměti Pythonu nutně uvolnit paměť zpět operačnímu systému. V dokumentaci se uvádí, že „za určitých okolností nemusí správce paměti Pythonu spustit příslušné akce, jako je garbage collection, kompakce paměti nebo jiná preventivní opatření.“

V důsledku toho může být nutné paměť v Pythonu explicitně uvolnit. Jedním ze způsobů, jak to provést, je přinutit správce odpadu Pythonu k uvolnění nepoužívané paměti pomocí modulu gc. K tomu stačí spustit příkaz gc.collect(). To však přináší znatelné výhody pouze při manipulaci s velmi velkým počtem objektů.

Kromě občasné chybovosti pythonovského garbage collectoru, zejména při práci s velkými soubory dat, je známo, že úniky paměti způsobuje také několik knihoven Pythonu. Jedním z takových nástrojů v hledáčku je například Pandas. Zvažte, zda se podívat na všechny problémy související s pamětí v oficiálním repozitáři pandas na GitHubu!

Jedním ze zřejmých důvodů, který může proklouznout i bystrým očím recenzentů kódu, je to, že v kódu přetrvávají velké objekty, které nejsou uvolněny. Stejně tak jsou dalším důvodem k obavám nekonečně rostoucí datové struktury. Například rostoucí datová struktura slovníku bez pevného omezení velikosti.

Jedním ze způsobů, jak vyřešit rostoucí datovou strukturu, je převést slovník pokud možno na seznam a nastavit maximální velikost seznamu. V opačném případě jednoduše nastavte limit pro velikost slovníku a vymažte jej vždy, když je limitu dosaženo.

Teď se možná ptáte, jak vůbec zjistit problémy s pamětí? Jednou z možností je využít nástroj pro sledování výkonu aplikací (APM). Kromě toho vám se sledováním a trasováním problémů s pamětí může pomoci mnoho užitečných modulů jazyka Python. Podívejme se na naše možnosti, počínaje nástroji APM.

Nástroje pro monitorování výkonu aplikací (APM)

Takže co přesně je monitorování výkonu aplikací a jak pomáhá při sledování problémů s pamětí? Nástroj APM umožňuje sledovat metriky výkonu programu v reálném čase a umožňuje průběžnou optimalizaci při odhalování problémů, které omezují výkon.

Na základě zpráv generovaných nástroji APM budete mít obecnou představu o tom, jak váš program funguje. Vzhledem k tomu, že můžete přijímat a sledovat metriky výkonu v reálném čase, můžete v případě zjištěných problémů okamžitě přijmout opatření. Jakmile zúžíte okruh možných oblastí programu, které mohou být viníky problémů s pamětí, můžete se ponořit do kódu a prodiskutovat jej s ostatními autory kódu, abyste dále určili konkrétní řádky kódu, které je třeba opravit.

Samotné dohledání příčin problémů s únikem paměti může být náročný úkol. Jeho oprava je další noční můrou, protože je třeba kódu skutečně porozumět. Pokud se někdy ocitnete v takové situaci, nehledejte dál, protože ScoutAPM je zdatný nástroj APM, který dokáže konstruktivně analyzovat a optimalizovat výkon vaší aplikace. ScoutAPM vám poskytne přehled v reálném čase, takže můžete rychle určit &řešit problémy dříve, než si jich mohou všimnout vaši klienti.

Profilové moduly

Existuje mnoho šikovných modulů Pythonu, které můžete použít k řešení problémů s pamětí, ať už se jedná o únik paměti nebo pád vašeho programu z důvodu nadměrného využití paměti. Dva z nich doporučujeme:

  1. tracemalloc
  2. memory-profiler

Poznamenejte, že pouze modul tracemalloc je vestavěný, takže pokud jej chcete používat, nezapomeňte nejprve nainstalovat druhý modul.

tracemalloc

Podle dokumentace Pythonu (3.9.0) pro tracemalloc vám použití tohoto modulu může poskytnout následující informace:

  • Zpětné sledování, kde byl objekt alokován.
  • Statistiky o alokovaných blocích paměti podle názvu souboru a čísla řádku: celková velikost, počet a průměrná velikost alokovaných bloků paměti.
  • Vypočítat rozdíl mezi dvěma snímky pro zjištění úniku paměti.

Doporučeným prvním krokem, který byste měli udělat při určování zdroje problému s pamětí, je nejprve zobrazit soubory, které alokují nejvíce paměti. To můžete snadno provést pomocí prvního příkladu kódu uvedeného v dokumentaci.

To však neznamená, že soubory, které alokují malé množství paměti, nebudou donekonečna narůstat a v budoucnu způsobovat úniky paměti.

memory_profiler

Tento modul je zábavný. Pracoval jsem s ním a je to můj osobní favorit, protože poskytuje možnost jednoduše přidat dekorátor @profile do libovolné funkce, kterou chcete zkoumat. Výstup poskytnutý jako výsledek je také velmi snadno pochopitelný.

Dalším důvodem, který z něj dělá mého osobního favorita, je, že tento modul umožňuje vykreslit graf využití paměti v závislosti na čase. Někdy prostě potřebujete rychle zkontrolovat, zda se využití paměti donekonečna zvyšuje, nebo ne. To je ideální řešení, protože k potvrzení této skutečnosti nemusíte provádět profilování paměti řádek po řádku. Můžete jednoduše pozorovat vykreslený graf poté, co necháte profilovač běžet po určitou dobu. Zde je příklad výstupního grafu –

Podle popisu v dokumentaci modulu memory-profiler slouží tento modul Pythonu ke sledování spotřeby paměti procesu a také k její řádkové analýze u programů Python. Jedná se o čistě pythonovský modul, který závisí na knihovně psutil.

Doporučuji přečíst si tento blog na médiu Medium, kde se dozvíte, jak se memory-profiler používá. Tam se také dozvíte, jak používat další modul Pythonu, muppy (nejnovější je muppy3).

Nejlepší postupy pro zlepšení výkonu kódu Pythonu

Všech podrobností o správě paměti už bylo dost. Nyní prozkoumáme některé dobré návyky při psaní paměťově úsporného kódu jazyka Python.

Využívejte knihovny a vestavěné funkce jazyka Python

Ano, toto je dobrý návyk, který může být poměrně často přehlížen. Jazyk Python má bezkonkurenční podporu komunity, což se projevuje množstvím knihoven Pythonu, které jsou k dispozici pro téměř jakýkoli účel, od volání API až po datovou vědu.

Pokud existuje knihovna Pythonu, která umožňuje dělat stejnou věc jako to, co jste již implementovali, můžete udělat to, že porovnáte výkon svého kódu při použití knihovny ve srovnání s výkonem při použití vlastního kódu. Je pravděpodobné, že knihovny Pythonu (zejména ty populární) budou paměťově úspornější než váš kód, protože jsou neustále vylepšovány na základě zpětné vazby komunity. Spoléhali byste se raději na kód, který byl vytvořen přes noc, nebo na kód, který byl po delší dobu důsledně vylepšován?

Nejlépe je, že knihovny Pythonu vám ušetří mnoho řádků kódu, tak proč ne?

Nepoužívání „+“ pro spojování řetězců

Všichni jsme se někdy provinili spojováním řetězců pomocí operátoru „+“, protože to vypadá tak jednoduše.

Všimněte si, že řetězce jsou neměnné. Proto při každém přidání prvku do řetězce pomocí operátoru „+“ musí Python vytvořit nový řetězec s novou alokací paměti. S delšími řetězci se paměťová neefektivita kódu projeví výrazněji.

Použití itertools pro efektivní cyklování

Cykly jsou nezbytnou součástí automatizace věcí. Jak budeme stále častěji používat smyčky, nakonec zjistíme, že musíme používat vnořené smyčky, o kterých je známo, že jsou neefektivní kvůli své vysoké časové náročnosti.

Tady přichází na pomoc modul itertools. Podle dokumentace k modulu itertools v jazyce Python: „Modul standardizuje základní sadu rychlých, paměťově efektivních nástrojů, které jsou užitečné samy o sobě nebo v kombinaci. Společně umožňují stručně a efektivně konstruovat specializované nástroje v čistém jazyce Python.“

Jinými slovy, modul itertools umožňuje paměťově efektivní cyklování tím, že se zbavuje zbytečných cyklů. Zajímavé je, že modul itertools je nazýván klenotem, protože umožňuje sestavovat elegantní řešení nesčetných problémů.

Jsem si téměř jistý, že ve svém příštím kódu budete pracovat alespoň s jednou smyčkou, tak zkuste implementovat itertools!“

Shrnutí a závěrečné myšlenky

Používání správných návyků správy paměti v jazyce Python není pro příležitostné programátory. Pokud si obvykle vystačíte s jednoduchými skripty, neměli byste na problémy spojené s pamětí vůbec narazit. Díky hardwaru a softwaru, které v době, kdy čtete tyto řádky, stále procházejí rychlým vývojem, by měl základní model téměř jakéhokoli zařízení bez ohledu na jeho značku bez problémů spouštět běžné programy. Potřeba paměťově úsporného kódu se začne projevovat až ve chvíli, kdy začnete pracovat na velké kódové základně, zejména pro produkční účely, kde je klíčový výkon.

To však neznamená, že správa paměti v jazyce Python je obtížně pochopitelný koncept, ani to neznamená, že není důležitá. Důraz na výkon aplikací totiž každým dnem roste. Jednoho dne už to nebude jen pouhá otázka „hotovo“. Místo toho budou vývojáři soutěžit o to, kdo dodá řešení, které bude nejen schopné úspěšně řešit potřeby zákazníků, ale také to udělá s bleskovou rychlostí a minimálními prostředky.