A szemétgyűjtési stratégiákkal kapcsolatos különbségek¶
A PyPy által használt vagy implementált szemétgyűjtők nem a referenciaszámoláson alapulnak, így az objektumok nem szabadulnak fel azonnal, amikor már nem elérhetők. Ennek legnyilvánvalóbb hatása az, hogy a fájlok (és socketek stb.) nem záródnak be azonnal, amikor kikerülnek a hatókörükből. Az írásra megnyitott fájlok esetében az adatok egy ideig a kimeneti pufferükben maradhatnak, így a lemezen lévő fájl üresnek vagy csonkának tűnik. Ráadásul előfordulhat, hogy elérjük az operációs rendszerünkben az egyidejűleg megnyitott fájlok számának korlátozását.
Ha olyan eset hibakeresése történik, amikor a programunkban egy fájl nem záródik be megfelelően, használhatjuk a -X track-resources
parancssori opciót. Ha ez meg van adva, akkor minden olyan fájlról és foglalatról, amelyet a szemétgyűjtő bezár, egy ResourceWarning
jelenik meg. A figyelmeztetés tartalmazza annak a pozíciónak a stack trace-ét, ahol a fájl vagy a socket létrejött, hogy könnyebb legyen látni, hogy a program mely részei nem zárják be explicit módon a fájlokat.
A CPythonban ezt a különbséget lényegében lehetetlen kijavítani anélkül, hogy a szemétgyűjtést a referenciaszámlálással történő megközelítésre kényszerítenénk. A hatás, amit a CPythonban kapunk, egyértelműen az implementáció mellékhatásaként és nem nyelvtervezési döntésként lett leírva: az erre támaszkodó programok alapvetően hamisak. Túl erős korlátozás lenne aCPython viselkedését egy nyelvi specifikációban kikényszeríteni, tekintve, hogy semmi esélye, hogy a Jython vagy az IronPython (vagy a Python bármely más portja Java-ra vagy.NET-re) átvegye.
Még a teljes GC kikényszerítésének naiv ötlete is nagyon rossz lehet bizonyos esetekben, amikor veszélyesen közel kerülünk az operációs rendszer határához. Ha a programod erősen szivárogtatja a nyitott fájlokat, akkor működne, de minden n-edik kiszivárgott fájlnál kényszeríts egy teljes GCciklust. Az n értéke egy konstans, de aprogram tetszőleges mennyiségű memóriát foglalhat, ami egy teljes GC-ciklust tetszőlegesen hosszúvá tesz. A végeredmény az, hogy a PyPy a futási idejének tetszőlegesen nagy hányadát töltené a GC-ben – lelassítva a tényleges végrehajtást, nem 10%-kal, nem 100%-kal, nem 1000%-kal, hanem lényegében bármilyen tényezővel.
A legjobb tudásunk szerint erre a problémára nincs jobb megoldás, mint a programok javítása. Ha 3rd-party kódban fordul elő, ez azt jelenti, hogy fel kell keresni a szerzőket, és el kell magyarázni nekik a problémát: be kell zárniuk a megnyitott fájljaikat ahhoz, hogy a Python bármely nem CPython-alapú implementációján fussanak.
Itt van még néhány technikai részlet. Ez a probléma a __del__
metódusok hívásának pontos idejét érinti, ami a PyPy-ben (sem a Jythonban, sem az IronPythonban) nem megbízható és nem időszerű. Ez azt is jelenti, hogy a gyenge hivatkozások a vártnál kicsit tovább maradhatnak életben. Ezáltal a “gyenge proxyk” (ahogyan a weakref.proxy()
visszaadja) kissé kevésbéhasznosak: úgy tűnik, hogy a PyPy-ben egy kicsit tovább maradnak életben, és hirtelen tényleg halottak lesznek, és a következő eléréskor ReferenceError
-t adnak ki. Minden gyenge proxyt használó kódnak gondosan el kell kapnia az ilyenReferenceError
-t minden olyan helyen, ahol használja őket. (Vagy még jobb, ha egyáltalán nem használszweakref.proxy()
; használj weakref.ref()
-t.)
Figyeljünk egy részletre a weakref visszahívások dokumentációjában:
Ha a visszahívás meg van adva és nem None, és a visszaadott weakrefobjektum még él, a visszahívás meg lesz hívva, amikor az objektum véglegesítésre kerül.
Vannak olyan esetek, amikor a CPython refcount szemantikája miatt egy weakrefdies közvetlenül az objektumok előtt vagy után, amelyekre mutat (jellemzően valamilyen körkörös hivatkozással). Ha történetesen közvetlenül utána hal meg, akkor a visszahívás meghívásra kerül. Hasonló esetben a PyPy-ben mind az objektum, mind a weakref egyszerre lesz halottnak tekintve, és a visszahívás nem lesz meghívva. (Issue #2030)
A GC-ben lévő különbségnek van néhány extra következménye. A legfontosabb, hogy ha egy objektumnak __del__
-je van, a __del__
soha nem hívódik meg többször a PyPy-ben; de a CPython többször is meg fogja hívni ugyanazt a __del__
-t, ha az objektum feltámad és újra meghal (legalábbis megbízhatóan így van ez a régebbi CPythonokban; az újabb CPythonok megpróbálják a destruktorokat nem hívni többször,de vannak ellenpéldák). A __del__
metódusokat a “megfelelő” sorrendben hívják meg, ha egymásra mutató objektumokon vannak, mint a CPythonban, de a CPythontól eltérően, ha van egy halott ciklusa egymásra hivatkozó objektumoknak, azok __del__
metódusait mindenképpen meghívják;a CPython ehelyett a gc
modul garbage
listájába tenné őket. További információ a blogon található .
Megjegyezzük, hogy ez a különbség bizonyos esetekben közvetve jelentkezhet. Például egy középen függőben hagyott generátor a PyPy-ben – ismét – később kerül szemétbe, mint a CPythonban. Láthatjuk a különbséget, ha a yield
kulcsszó, amelynél felfüggesztették, maga is egy try:
vagy egy with:
blokkba van zárva. Ez például a 736. problémaként jelenik meg.
Az alapértelmezett GC (minimark
nevű) használatával a beépített függvény id()
úgy működik, mint a CPythonban. Más GC-vel olyan számokat ad vissza, amelyek nem valódi címek (mert egy objektum többször is mozoghat)és a sok hívása teljesítményproblémához vezethet.
Megjegyezzük, hogy ha objektumok hosszú láncolata van, mindegyiknek van egy referenciája a következőre, és mindegyiknek van egy __del__
, a PyPy GC rosszul fog teljesíteni. A pozitív oldalon, a legtöbb más esetben a benchmarkok azt mutatják, hogy a PyPy GC-je sokkal jobban teljesít, mint a CPythoné.
Egy másik különbség, hogy ha egy meglévő osztályhoz __del__
-t csatolsz, az nem lesz meghívva:
>>>> class A(object):.... pass....>>>> A.__del__ = lambda self: None__main__:1: RuntimeWarning: a __del__ method added to an existing type will not be called
Még homályosabb: ugyanez igaz, a régi stílusú osztályokra, ha egy példányhoz csatolod a __del__
-t (ez még a CPythonban sem működik azúj stílusú osztályokkal). A PyPy-ben RuntimeWarning-et kapunk. Ezeknek az eseteknek a javításához csak győződjünk meg róla, hogy van egy __del__
metódus az osztályban, amivel kezdjük (még ha csak pass
-t tartalmaz is; ennek helyettesítése vagy felülbírálása később jól működik).
Utolsó megjegyzés: a CPython megpróbál automatikusan gc.collect()
-t csinálni, amikor a program befejeződik; a PyPy nem. (Mind a CPythonban, mind a PyPy-ben lehetséges olyan eset, amikor több gc.collect()
-re van szükség, mielőtt az összes objektum meghal. Emiatt a CPython megközelítése amúgy is csak “legtöbbször” működik.)