Verschillen tussen PyPy en CPython¶

Verschillen gerelateerd aan garbage collection strategieën¶

De garbage collectors gebruikt of geïmplementeerd door PyPy zijn niet gebaseerd op het tellen van referenties, dus de objecten worden niet direct vrijgemaakt wanneer ze niet meer bereikbaar zijn. Het meest voor de hand liggende effect hiervan is dat bestanden (en sockets, etc) niet onmiddellijk worden gesloten wanneer ze buiten bereik gaan. Voor bestanden die zijn geopend voor schrijven, kunnen gegevens een tijdje in hun uitvoerbuffers blijven zitten, waardoor het bestand op de schijf leeg of afgekapt lijkt. Bovendien kunt u de limiet van uw besturingssysteem bereiken voor het aantal gelijktijdig geopende bestanden.

Als u een geval aan het debuggen bent waarbij een bestand in uw programma niet goed wordt gesloten, kunt u de -X track-resources command line optie gebruiken. Als deze wordt gegeven, wordt een ResourceWarning geproduceerd voor elk bestand en socket dat de vuilnisman sluit. De waarschuwing bevat de stack trace van de positie waar het bestand of de socket is aangemaakt, om het makkelijker te maken om te zien welke delen van het programma bestanden niet expliciet sluiten.

Dit verschil in CPython oplossen is in wezen onmogelijk zonder eenference-counting benadering van garbage collection te forceren. Het effect dat je in CPython krijgt is duidelijk beschreven als een neveneffect van de implementatie en niet als een taalontwerpbeslissing: programma’s die hierop vertrouwen zijn in principe onzin. Het zou een te sterke beperking zijn om te proberen het gedrag vanCPython af te dwingen in een taalspecificatie, gegeven het feit dat het geen kans heeft om te worden overgenomen door Jython of IronPython (of een andere port van Python naar Java of.NET).

Zelfs het naïeve idee om een volledige GC te forceren als we gevaarlijk dicht bij de limiet van het OS komen, kan in sommige gevallen erg slecht zijn. Als je programma veel open bestanden lekt, dan zou het werken, maar forceer een volledige GC-cyclus voor elk n’e gelekt bestand. De waarde van n is een constante, maar het programma kan een willekeurige hoeveelheid geheugen innemen, wat een complete GC-cyclus willekeurig lang maakt. Het eindresultaat is dat PyPy een willekeurig groot deel van zijn tijd in de GC zou spenderen – de eigenlijke uitvoering vertragend, niet met 10% of 100% of 1000% maar met in wezen om het even welke factor.

Voor zover wij weten heeft dit probleem geen betere oplossing dan de programma’s te repareren. Als het zich voordoet in code van derden, betekent dit dat u naar de auteurs moet gaan en hun het probleem moet uitleggen: zij moeten hun open bestanden sluiten om te kunnen draaien op een niet op Python gebaseerde implementatie van Python.

Hier zijn wat meer technische details. Dit probleem heeft invloed op de precieze tijd waarop __del__methoden worden aangeroepen, wat niet betrouwbaar of tijdig is in PyPy (noch Jython, noch IronPython). Het betekent ook dat zwakke referenties iets langer in leven kunnen blijven dan verwacht. Dit maakt “zwakke proxies” (zoals geretourneerd door weakref.proxy()) iets minder bruikbaar: ze lijken iets langer in leven te blijven in PyPy, en plotseling zullen ze echt dood zijn en een ReferenceError oproepen bij de volgende toegang. Elke code die zwakke proxies gebruikt moet zorgvuldig zulkeReferenceError opvangen op elke plaats die ze gebruikt. (Of, nog beter, gebruikweakref.proxy() helemaal niet; gebruik weakref.ref().)

Noteer een detail in de documentatie voor weakref callbacks:

Als de callback is opgegeven en niet None, en het geretourneerde weakrefobject is nog in leven, dan zal de callback worden aangeroepen wanneer het object op het punt staat om te worden gefinaliseerd.

Er zijn gevallen waarin, als gevolg van de refcount-semantiek van CPython, een zwak ref-object onmiddellijk voor of na de objecten waarnaar het verwijst sterft (meestal met een circulaire verwijzing). Als hij toevallig net daarna sterft, dan zal de callback worden aangeroepen. In een soortgelijk geval in PyPy, worden zowel het object als de weakref tegelijkertijd als dood beschouwd, en wordt de callback niet aangeroepen. (Issue #2030)

Er zijn een paar extra implicaties van het verschil in de GC. Het belangrijkste is dat als een object een __del__ heeft, de __del__ nooit meer dan één keer wordt aangeroepen in PyPy; maar CPython zal dezelfde __del__ meerdere keren aanroepen als het object herrijst en weer sterft (tenminste, dat is betrouwbaar in oudere CPythons; nieuwere CPythons proberen destructors niet meer dan één keer aan te roepen, maar er zijn tegen-exemplaren). De __del__ methoden worden in “de juiste” volgorde aangeroepen als ze op objecten staan die naar elkaar verwijzen, zoals in CPython, maar in tegenstelling tot CPython, als er een dode cyclus is van objecten die naar elkaar verwijzen, worden hun __del__ methoden toch aangeroepen;CPython zou ze in plaats daarvan in de lijst garbage van de gcmodule zetten. Meer informatie is beschikbaar op de blog.

Merk op dat dit verschil in sommige gevallen indirect zichtbaar kan zijn. Bijvoorbeeld, een generator die in het midden wordt gelaten, wordt – alweer – later opgehaald in PyPy dan in CPython. Je kunt het verschil zien als het yield sleutelwoord waar hij aan hangt zelf ingesloten is in een try: of een with: blok. Dit komt bijvoorbeeld naar voren als probleem 736.

Gebruikt u de standaard GC (genaamd minimark), dan werkt de ingebouwde functie id() zoals het in CPython doet. Met andere GC’s geeft het getallen terug die geen echte adressen zijn (omdat een object verschillende keren kan bewegen) en het veelvuldig aanroepen ervan kan leiden tot prestatieproblemen.

Merk op dat als je een lange keten van objecten hebt, elk met een verwijzing naar de volgende, en elk met een __del__, PyPy’s GC slecht zal presteren. Aan de andere kant, in de meeste andere gevallen, hebben benchmarks aangetoond dat PyPy’s GC veel beter presteert dan die van CPython.

Een ander verschil is dat als je een __del__ aan een bestaande class toevoegt, deze niet zal worden aangeroepen:

>>>> class A(object):.... pass....>>>> A.__del__ = lambda self: None__main__:1: RuntimeWarning: a __del__ method added to an existing type will not be called

Een nog onduidelijker verschil: hetzelfde geldt, voor old-style classes, als je de __del__ aan een instantie vastmaakt (zelfs in CPython werkt dit niet met new-style classes). Je krijgt een RuntimeWarning in PyPy. Om deze gevallen op te lossen, moet je er gewoon voor zorgen dat er een __del__ methode in de klasse zit om mee te beginnen (zelfs als die alleen pass bevat; later vervangen of overschrijven werkt prima).

Laatste opmerking: CPython probeert automatisch een gc.collect() te doen als het programma eindigt; PyPy niet. (Het is mogelijk in zowel CPython als PyPy om een geval te ontwerpen waar meerdere gc.collect() nodig zijn voordat alle objecten sterven. Dit maakt dat CPython’s aanpak toch maar “meestal” werkt.)

Geef een antwoord

Het e-mailadres wordt niet gepubliceerd.