Különbségek a PyPy és a CPython között¶

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 gcmodul 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.)

Vélemény, hozzászólás?

Az e-mail-címet nem tesszük közzé.