Rozdíly týkající se strategií sběru odpadu¶
Sběrače odpadu používané nebo implementované PyPy nejsou založeny na počítání odkazů, takže objekty nejsou uvolněny okamžitě, když už nejsou dosažitelné. Nejzřetelnějším důsledkem toho je, že soubory (a sokety atd.) nejsou okamžitě uzavřeny, když se dostanou mimo obor. U souborů, které jsou otevřeny pro zápis, mohou data zůstat chvíli ležet ve výstupních bufferech, takže soubor na disku vypadá prázdný nebo zkrácený. Navíc můžete dosáhnout limitu vašeho operačního systému na počet současně otevřených souborů.
Pokud ladíte případ, kdy soubor ve vašem programu není správně uzavřen, můžete použít volbu příkazového řádku -X track-resources
. Pokud ji zadáte, bude pro každý soubor a soket, který sběrač odpadků zavře, vytvořen ResourceWarning
. Výstraha bude obsahovat stopu zásobníku v místě, kde byl soubor nebo soket vytvořen, aby bylo snazší zjistit, kteréčásti programu neuzavírají soubory explicitně.
Oprava tohoto rozdílu v CPythonu je v podstatě nemožná bez vynucení přístupu ke sběru odpadků pomocí počítání odkazů. Efekt, který získáte v CPythonu, byl jasně popsán jako vedlejší efektimplementace, nikoliv jako rozhodnutí o návrhu jazyka: programy, které na něj spoléhají, jsou v podstatě falešné. Bylo by příliš silným omezením snažit se prosadit chováníCPythonu ve specifikaci jazyka, vzhledem k tomu, že nemá šanci, aby jej převzal Jython nebo IronPython (nebo jakýkoli jiný port Pythonu do Javy nebo.NET).
V některých případech může být velmi špatná i naivní myšlenka vynucení plného GC, když se nebezpečně blížíme limitu operačního systému. Pokud váš programpouští otevřené soubory ve velké míře, pak by to fungovalo, ale vynutit kompletní GCcyklus každý n’tý uniklý soubor. Hodnota n je konstanta, ale program může zabírat libovolné množství paměti, což činí kompletní GC cyklus libovolně dlouhým. Konečným výsledkem je, že PyPy by strávil libovolně velkou část svého běhu v GC – zpomalil by skutečné provádění ne o 10 % ani o 100 % ani o 1000 %, ale v podstatě o jakýkoli faktor.
Podle našich znalostí nemá tento problém lepší řešení než opravu programů. Pokud se vyskytuje v kódu třetích stran, znamená to zajít za autory a vysvětlit jim problém: musí zavřít své otevřené soubory, aby mohly běžet na jakékoli implementaci Pythonu, která není založena na Pythonu.
Tady jsou další technické podrobnosti. Tento problém se týká přesnéhočasu, ve kterém jsou volány __del__
metody, což v PyPy (ani v Jythonu, ani v IronPythonu) není spolehlivé ani včasné. Znamená to také, že slabé reference mohou zůstat naživu o něco déle, než se očekává. Tozpůsobuje, že „slabé odkazy“ (vracené pomocí weakref.proxy()
) jsou poněkud méněužitečné: v PyPy se bude zdát, že zůstávají naživu o něco déle, a najednou budou skutečně mrtvé a při dalším přístupu vyvolají ReferenceError
. Každý kód, který používá slabé zástupce, musí pečlivě zachytit takové ReferenceError
na každém místě, které je používá. (Nebo ještě lépe, nepoužívejteweakref.proxy()
vůbec; použijte weakref.ref()
.)
Všimněte si detailu v dokumentaci pro zpětná volání weakref:
Pokud je poskytnuto zpětné volání a není None a vrácený weakrefobjekt je stále živý, bude zpětné volání zavoláno, když se objekt chystá být finalizován.
Existují případy, kdy kvůli sémantice CPython refcount slabýrefobjekt skončí bezprostředně před nebo za objekty, na které ukazuje (typickys nějakým kruhovým odkazem). Pokud se stane, že zemře těsně za ním, bude vyvoláno zpětné volání. V podobném případě v PyPy bude objekt i weakref považován za mrtvý současně a zpětné volání nebude vyvoláno. (Issue #2030)
Z rozdílu v GC vyplývá několik dalších důsledků. Především, pokud má objekt __del__
, není tento __del__
v PyPy nikdy volán více než jednou; ale CPython bude volat stejný __del__
několikrát, pokud je objekt vzkříšen a znovu zemře (alespoň je to tak spolehlivě ve starších CPythonech; novější CPythony se snaží volat destruktory ne více než jednou,ale existují protipříklady). Metody __del__
jsou volány ve „správném“ pořadí, pokud jsou na objektech odkazujících na sebe navzájem, jako v CPythonu, ale na rozdíl od CPythonu, pokud je mrtvý cyklus objektů odkazujících na sebe navzájem, jsou jejich metody __del__
volány tak jako tak;CPython by je místo toho vložil do seznamu garbage
modulu gc
. Více informací je k dispozici na blogu .
Všimněte si, že tento rozdíl se může v některých případech projevit nepřímo. Například generátor, který zůstal nevyřízený uprostřed, je – opět – v PyPy sbírán později než v CPythonu. Rozdíl je vidět, pokud je klíčové slovo yield
, na kterém je pozastaveno, samo uzavřeno v bloku try:
nebo with:
. To se projevuje například jako problém 736.
Při použití výchozího GC (nazvaného minimark
) funguje vestavěná funkce id()
stejně jako v CPythonu. S jinými GC vrací čísla, která nejsou skutečnými adresami (protože objekt se může několikrát přesunout)a její časté volání může vést k problémům s výkonem.
Všimněte si, že pokud máte dlouhý řetězec objektů, každý s odkazem na další a každý s __del__
, bude GC PyPy fungovat špatně. Na druhou stranu, ve většině ostatních případů benchmarky ukázaly, že GC PyPy funguje mnohem lépe než GC CPythonu.
Další rozdíl je v tom, že pokud přidáte __del__
k existující třídě, nebude se volat:
>>>> class A(object):.... pass....>>>> A.__del__ = lambda self: None__main__:1: RuntimeWarning: a __del__ method added to an existing type will not be called
Ještě nejasnější: totéž platí pro třídy starého stylu, pokud k instanci připojíte __del__
(ani v CPythonu to nefunguje u tříd nového stylu). V PyPy dostanete RuntimeWarning. Chcete-li tyto případy vyřešit, stačí se ujistit, že ve třídě je na začátku metoda __del__
(i když obsahuje pouze pass
; její pozdější nahrazení nebo přepsání funguje dobře).
Poslední poznámka: CPython se snaží provést gc.collect()
automaticky, když program skončí; ne PyPy. (Jak v CPythonu, tak v PyPy je možné navrhnout případ, kdy je potřeba několik gc.collect()
, než všechny objektyzaniknou. Díky tomu přístup CPythonu stejně funguje jen „většinu času“)
.