Skillnader mellan PyPy och CPython¶

Skillnader relaterade till skräpinsamlingsstrategier¶

De skräpinsamlare som används eller implementeras av PyPy är inte baserade på referenstagning, så objekten frigörs inte omedelbart när de inte längre kan nås. Den mest uppenbara effekten av detta är att filer (och sockets etc.) inte stängs omedelbart när de inte längre är tillgängliga. För filer som öppnas för att skrivas kan data lämnas kvar i deras utdatapuffer ett tag, vilket gör att filen på disken ser tom eller avkortad ut. Dessutom kan det hända att du når operativsystemets gräns för antalet samtidigt öppnade filer.

Om du felsöker ett fall där en fil i ditt program inte stängs korrekt kan du använda kommandoradsalternativet -X track-resources. Om det ges produceras en ResourceWarning för varje fil och socket som garbage collector stänger. Varningen kommer att innehålla stacktrace för den position där filen eller sockeln skapades, för att göra det lättare att se vilka delar av programmet som inte stänger filer explicit.

Att åtgärda den här skillnaden i CPython är i princip omöjligt utan att tvinga fram ett referensräknande tillvägagångssätt för skräpplockning. Effekten som du får i CPython har tydligt beskrivits som en bieffekt av implementeringen och inte som ett beslut om språkets utformning: program som förlitar sig på detta är i princip oseriösa. Det skulle vara en alltför stark begränsning att försöka genomdrivaCPythons beteende i en språkspecifikation, med tanke på att det inte har någon chans att antas av Jython eller IronPython (eller någon annan anpassning av Python till Java eller NET).

Även den naiva idén att tvinga fram en fullständig GC när vi kommer farligt nära operativsystemets gräns kan vara mycket dålig i vissa fall. Om ditt program läcker öppna filer i stor utsträckning skulle det fungera, men tvinga fram en fullständig GC-cykel för varje nionde läckta fil. Värdet n är en konstant, men programmet kan ta en godtycklig mängd minne i anspråk, vilket gör en fullständig GC-cykel godtyckligt lång. Slutresultatet är att PyPy skulle spendera en godtyckligt stor del av sin körtid i GC – vilket skulle sakta ner den faktiska utförandet, inte med 10 % eller 100 % eller 1000 % utan med i princip vilken faktor som helst.

Såvitt vi vet finns det ingen bättre lösning på detta problem än att rätta till programmen. Om det förekommer i kod från tredje part innebär det att man måste gå till författarna och förklara problemet för dem: de måste stänga sina öppna filer för att kunna köras på alla icke-Python-baserade implementationer av Python.

Här är några fler tekniska detaljer. Detta problem påverkar den exakta tidpunkt då __del__ metoder anropas, vilket inte är tillförlitligt eller lägligt i PyPy (och inte heller i Jython eller IronPython). Det innebär också att svaga referenser kan hålla sig vid liv lite längre än väntat. Detta gör ”weak proxies” (som returneras av weakref.proxy()) något mindre användbara: de ser ut att hålla sig vid liv lite längre i PyPy, och plötsligt är de verkligen döda och ger upphov till en ReferenceError vid nästa åtkomst. Kod som använder svaga proxies måste noggrant fånga upp sådana ReferenceError på alla ställen där de används. (Eller, ännu bättre, använd inteweakref.proxy() alls; använd weakref.ref().)

Notera en detalj i dokumentationen för weakref callbacks:

Om callback tillhandahålls och inte None, och det returnerade weakrefobjektet fortfarande lever, kommer callback att anropas när objektet är på väg att slutföras.

Det finns fall där, på grund av CPythons refcount-semantik, en weakrefdies omedelbart före eller efter de objekt den pekar på (typiskt med någon cirkulär referens). Om den råkar dö precis efter kommer callbacken att anropas. I ett liknande fall i PyPy kommer både objektet och den svaga referensen att betraktas som döda samtidigt,och callbacken kommer inte att åberopas. (Problem nr 2030)

Det finns några extra konsekvenser av skillnaden i GC. Det viktigaste är att om ett objekt har en __del__ så anropas aldrig __del__ mer än en gång i PyPy, men CPython anropar samma __del__ flera gånger om objektet återuppstår och dör igen (åtminstone är det tillförlitligt så i äldre CPythons; nyare CPythons försöker att inte anropa destruktorer mer än en gång, men det finns motbevis). __del__-metoderna anropas i ”rätt” ordning om de finns på objekt som pekar på varandra, som i CPython, men till skillnad från CPython anropas deras __del__-metoder ändå om det finns en död cykel av objekt som hänvisar till varandra; CPython skulle i stället placera dem i listan garbage i gcmodulen. Mer information finns på bloggen .

Bemärk att denna skillnad kan visa sig indirekt i vissa fall. Exempelvis är en generator som lämnas i väntan i mitten – återigen – sopor som samlas in senare i PyPy än i CPython. Du kan se skillnaden om nyckelordet yield som den är uppskjuten vid i sin tur är innesluten i ett try: eller ett with: block. Detta visar sig till exempel som problem 736.

Med standard-GC (kallad minimark) fungerar den inbyggda funktionen id() som den gör i CPython. Med andra GC:er returnerar den siffror som inte är riktiga adresser (eftersom ett objekt kan flyttas runt flera gånger) och om den anropas ofta kan det leda till prestandaproblem.

Bemärk att om du har en lång kedja av objekt, vart och ett med en referens till nästa, och vart och ett med en __del__, kommer PyPys GC att prestera dåligt. På den ljusa sidan, i de flesta andra fall har benchmarks visat att PyPys GC presterar mycket bättre än CPythons.

En annan skillnad är att om du lägger till en __del__ till en befintlig klass kommer den inte att kallas:

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

Ännu mer obskyrt: samma sak gäller för klasser i gammal stil om du fäster __del__ till en instans (inte ens i CPython fungerar detta med klasser i ny stil). Du får en RuntimeWarning i PyPy. För att åtgärda dessa fall ska du bara se till att det finns en __del__-metod i klassen till att börja med (även om den bara innehåller pass; att ersätta eller åsidosätta den senare fungerar bra).

Sista anmärkningen: CPython försöker göra en gc.collect() automatiskt när programmet avslutas; inte PyPy. (Det är möjligt i både CPython och PyPy att utforma ett fall där flera gc.collect() behövs innan alla objektdör. Detta gör att CPythons tillvägagångssätt ändå bara fungerar ”för det mesta”.)

Lämna ett svar

Din e-postadress kommer inte publiceras.