Forskelle mellem PyPy og CPython¶

Forskelle i forbindelse med strategier for affaldsopsamling¶

Den affaldsopsamler, der anvendes eller implementeres af PyPy, er ikke baseret på referencetælling, så objekterne frigives ikke øjeblikkeligt, når de ikke længere kan nås. Den mest indlysende effekt af dette er, at filer (og sockets osv.) ikke straks lukkes, når de går ud af rækkevidde. For filer, der er åbnet til skrivning, kan data blive siddende i deres outputbuffere i et stykke tid, hvilket får filen på disken til at se tom eller afkortet ud. Desuden kan du nå dit OS’s grænse for antallet af samtidigt åbne filer.

Hvis du debugger et tilfælde, hvor en fil i dit program ikke er lukket korrekt, kan du bruge kommandolinjeindstillingen -X track-resources. Hvis den er givet, produceres der en ResourceWarning for hver fil og socket, som garbage collector lukker. Advarslen vil indeholde staksporet for den position, hvor filen eller socket blev oprettet, for at gøre det lettere at se, hvilke dele af programmet der ikke lukker filer eksplicit.

Fixing this difference to CPython is essentially impossible without forcing areference-counting approach to garbage collection. Den effekt, man får i CPython, er klart blevet beskrevet som en bivirkning af implementeringen og ikke en beslutning om sprogdesign: programmer, der er afhængige af dette, er i bund og grund falske. Det ville være en alt for stærk begrænsning at forsøge at gennemtvingeCPythons adfærd i en sprogspecifikation, da den ikke har nogen chance for at blive overtaget af Jython eller IronPython (eller nogen anden tilpasning af Python til Java eller.NET).

Selv den naive idé om at fremtvinge en fuld GC, når vi kommer faretruende tæt på OS’ets grænse, kan være meget dårlig i nogle tilfælde. Hvis dit program lækker åbne filer i høj grad, så ville det virke, men tvinge en komplet GC-cyklus hver n’te lækkede fil. Værdien af n er en konstant, men programmet kan tage en vilkårlig mængde hukommelse, hvilket gør en komplet GC-cyklus vilkårligt lang. Slutresultatet er, at PyPy ville bruge en vilkårligt stor del af sin køretid i GC’en – hvilket ville forsinke den faktiske udførelse, ikke med 10 % eller 100 % eller 1000 %, men med stort set en vilkårlig faktor.

Så vidt vi ved, har dette problem ingen bedre løsning end at rette programmerne. Hvis det forekommer i kode fra tredjepart, betyder det, at man skal henvende sig til forfatterne og forklare dem problemet: de skal lukke deres åbne filer for at kunne køre på enhver ikke-CPython-baseret implementering af Python.

Her er nogle flere tekniske detaljer. Dette problem påvirker det præcise tidspunkt, hvor __del__ metoder kaldes, hvilket ikke er pålideligt eller rettidigt i PyPy (og heller ikke i Jython eller IronPython). Det betyder også, at svage referencer kan forblive i live i lidt længere tid end forventet. Dette gør “svage proxies” (som returneret af weakref.proxy()) noget mindre nyttige: de ser ud til at forblive i live i lidt længere tid i PyPy, og pludselig vil de i virkeligheden være døde og give anledning til en ReferenceError ved næste adgang. Enhver kode, der bruger svage proxies, skal omhyggeligt fange sådanneReferenceError på ethvert sted, der bruger dem. (Eller, endnu bedre, lad være med at brugeweakref.proxy() overhovedet; brug weakref.ref().)

Bemærk en detalje i dokumentationen for weakref callbacks:

Hvis callback er angivet og ikke None, og det returnerede weakrefobjekt stadig er i live, vil callbacken blive kaldt, når objektet er ved at blive færdiggjort.

Der er tilfælde, hvor der på grund af CPythons refcount-semantik er en weakrefdies umiddelbart før eller efter de objekter, som den peger på (typiskmed en eller anden cirkulær reference). Hvis den tilfældigvis dør lige efter, så vil callbacken blive påkaldt. I et lignende tilfælde i PyPy vil både objektet og weakref’en blive betragtet som døde på samme tid, og callback’en vil ikke blive påkaldt. (Problem #2030)

Der er et par ekstra implikationer fra forskellen i GC’en. Det vigtigste er, at hvis et objekt har en __del__, bliver __del__ aldrig kaldt mere end én gang i PyPy; men CPython vil kalde den samme __del__ flere gange, hvis objektet genopstår og dør igen (i det mindste er det pålideligt sådan i ældre CPythons; nyere CPythons forsøger at kalde destruktorer ikke mere end én gang, men der findes modeksempler). __del__-metoderne kaldes i “den rigtige” rækkefølge, hvis de er på objekter, der peger på hinanden, som i CPython, men i modsætning til CPython kaldes deres __del__-metoder alligevel, hvis der er en død cyklus af objekter, der refererer til hinanden; CPython ville i stedet sætte dem ind i listen garbage i gcmodulet. Der findes flere oplysninger på bloggen .

Bemærk, at denne forskel kan vise sig indirekte i nogle tilfælde. Forexample, en generator, der er efterladt afventende i midten, er – igen – affaldsindsamlet senere i PyPy end i CPython. Du kan se forskellen, hvis det yield nøgleord, som den er suspenderet ved, selv er omsluttet af en try: eller en with: blok. Dette viser sig f.eks. som problem 736.

Ved brug af standard GC (kaldet minimark) fungerer den indbyggede funktion id() som i CPython. Med andre GC’er returnerer den tal, der ikke er rigtige adresser (fordi et objekt kan flytte rundt flere gange), og hvis du kalder den meget, kan det føre til problemer med ydeevnen.

Bemærk, at hvis du har en lang kæde af objekter, der hver især har en reference til det næste objekt og hver især har en __del__, vil PyPy’s GC fungere dårligt. På den lyse side har benchmarks i de fleste andre tilfælde vist, at PyPy’s GC’s præsterer meget bedre end CPython’s GC’s.

En anden forskel er, at hvis du tilføjer en __del__ til en eksisterende klasse, vil den ikke blive kaldt:

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

Endnu mere obskurt: det samme gælder for klasser i gammel stil, hvis du tilknytter __del__ til en instans (selv i CPython fungerer dette ikke med klasser i ny stil). Du får en RuntimeWarning i PyPy. For at løse disse tilfælde skal du blot sørge for, at der er en __del__-metode i klassen til at starte med (selv hvis den kun indeholder pass; det fungerer fint at erstatte eller overskrive den senere).

Sidste bemærkning: CPython forsøger at lave en gc.collect() automatisk, når programmet afsluttes; ikke PyPy. (Det er muligt i både CPython og PyPy at designe et tilfælde, hvor der er behov for flere gc.collect(), før alle objekter dør. Dette gør, at CPythons fremgangsmåde alligevel kun fungerer “det meste af tiden”.)

Skriv et svar

Din e-mailadresse vil ikke blive publiceret.