Sběr odpadků

19. 11. 2011 15:44 (aktualizováno) zboj

Vzhledem k serveru, na kterém toto čtete, je asi každému zřejmé, že tento článek není o komunálních službách. Ano, uhodli jste, jde o garbage collection (GC).

Především je nutné zmínit, že správa zdrojů se netýká jen paměti, ale například i souborů, připojení k databázi apod. GC řeší pouze správu paměti, o ostatní zdroje musí postarat vývojář. Většina vývojářů, i začínajících, na otázku, co je GC, odpoví, že „část programu nebo běhového prostředí, která prochází hierarchii objektů od kořenů, označuje ty dosažitelné a likviduje ostatní“. To je ovšem jako tvrdit, že hasič je člověk, který jezdí v červeném autě a stříká vodu. Odhlédneme-li od konkrétních implementací, úkolem GC je umožnit opětovné využití paměti, která již není potřebná.

Nejjednodušším GC je tzv. „null garbage collector“, který nedělá vůbec nic. Pokud bychom měli v počítači neomezné množství paměť, byl by jednoznačně nejlepším. Prakticky každý program, který nepotřebuje více paměti, než je fyzicky k dispozici, GC nepotřebuje. Není to sice fér k ostatním aplikacím, protože OS je buď odsune do virtuální paměti, nebo ukončí (některé OS, zejména mobilní, virtuální paměť nemají vůbec), ale vlastní program korektně proběhne a skončí.

Problém se správou paměti je, že v postatě každý program je natolik komplexní, že vývojář ztrácí o paměti přehled. Ohledně správy paměti existují dva extrémy. Ve většině případů je ideální uvolňovat paměť hned, jakmile není potřebná (objekt už nepotřebujeme). Druhým extrémem je neuvolňovat vůbec.

Ideál téměř bezchybně splňuje alokace na zásobníku, až na to, že takový objekt vždy „žije“ jen chvíli, lokálně, neexistuje způsob, jak jeho život prodloužit, pokud jej potřebujeme i jinde. Řešením je takové objekty kopírovat, buď ze zásobníku na haldu (jako u bloků v clangu/LLVM), nebo opět na zásobník. Kopírování je pochopitelně relativně drahé, i když například při návratu z funkce moderní překladače return optimalizují a nic se nekopíruje. (Doporučuji experiment s třídou vector v nativní STL a STL/CLR, výsledek bude v obou případech v MSIL, ale STL/CLR je mnohem rychlejší – ne algoritmicky, ale proto, že se využívají jen řízená data, a navíc odpadá dealokace).

Objekty na haldě jsou mnohem dražší. Ne nutně alokace (záleží na použitém alokátoru nebo GC), ale i rušení objektů nebo kopírování referencí. Blízko ideálu  je autorelease pool, prvně použitý v OpenStepu (původní NeXTStep nic takového neměl). Nepotřebné objekty se sice neruší v okamžiku, kdy už nejsou potřebné (což by bylo ideální z hlediska spotřeby paměti), ale aspoň se ruší deterministicky (i když ne z pohledu programátora). Díky rozdělení paměti na zóny (tehdy, dnešní runtime už funguje jinak) navíc nebyly problémy s defragmentací.

Plně automatické GC alokují rychle a dobré implementace i uvolňují rychle. To je vykoupeno obrovskou složitostí implementace (např. .NET má dva zásobníky a pět hald), to ale programátora píšícího běžné aplikace moc zajímat nemusí. Větším problémem je paměťová náročnost (angl. memory footprint). Empiricky je dokázáno, že systém s GC dosáhne stejného výkonu jako stejný kód bez GC, pokud má k dispozici 5× více paměti. Prakticky ovšem mírně nižší výkon téměř nikdy nevadí a zmíněný fakt je tak čistě akademický. Navíc paměti je v počítačích dost (méně už na mobilních zařízeních, kde navíc čím více paměti, tím větší spotřeba energie).

Dost bylo teorie, jaké jsou rady pro efektivní využívání paměti? To silně závisí na jazyku. V C++ používat RAII a const („copy semantics“ ne vždy kopíruje, např. kolekce optimalizují kopírování klíčů a hashů u neměnitelných objektů), v .NET používat „value“ typy (většinou vznikají na zásobníku a jejich životní cyklus proto není řízen GC), v C++/CLI používat „stack semantics“ (to sice nemá nic společného se zásobníkem, „ref“ třídy vznikají vždy na haldě, ale aspoň zajistíme deterministickou správu nepaměťových zdrojů). V Javě se toho moc dělat nedá, ale nejnovější verze překladače zásobník používá podle vlastního uvážení (s využitím escape analýzy).

NB: Plně automatický GC není nic nového, měly jej např. Lisp či Smalltalk.

Sdílet