Differenze tra PyPy e CPython¶

Differenze relative alle strategie di garbage collection¶

I garbage collector usati o implementati da PyPy non sono basati sul conteggio dei riferimenti, quindi gli oggetti non sono liberati istantaneamente quando non sono più raggiungibili. L’effetto più ovvio di questo è che i file (e i socket, ecc.) non vengono prontamente chiusi quando vanno fuori portata. Per i file che sono aperti per la scrittura, i dati possono essere lasciati nei loro buffer di uscita per un po’, facendo apparire il file sul disco vuoto o troncato. Inoltre, potreste raggiungere il limite del vostroOS sul numero di file aperti simultaneamente.

Se state facendo il debug di un caso in cui un file nel vostro programma non viene chiuso correttamente, potete usare l’opzione -X track-resources della linea di comando. Se viene data, viene prodotto un ResourceWarning per ogni file e socket che il garbage collector chiude. L’avvertimento conterrà la traccia dello stack della posizione in cui il file o il socket è stato creato, per rendere più facile vedere quali parti del programma non chiudono esplicitamente i file.

Mettere a posto questa differenza in CPython è essenzialmente impossibile senza forzare un approccio alla garbage collection basato sul conteggio delle referenze. L’effetto che si ottiene in CPython è stato chiaramente descritto come un effetto collaterale dell’implementazione e non una decisione di progettazione del linguaggio: i programmi che si basano su questo sono fondamentalmente fasulli. Sarebbe una restrizione troppo forte cercare di imporre il comportamento diCPython in una specifica di linguaggio, dato che non ha alcuna possibilità di essere adottata da Jython o IronPython (o qualsiasi altro port di Python su Java o.NET).

Anche l’idea ingenua di forzare un GC completo quando ci stiamo avvicinando pericolosamente al limite del sistema operativo può essere molto negativa in alcuni casi. Se il vostro programma perde pesantemente i file aperti, allora funzionerebbe, ma forzare un ciclo GC completo ogni n file che perde. Il valore di n è una costante, ma il programma può prendere una quantità arbitraria di memoria, il che rende un ciclo GC completo arbitrariamente lungo. Il risultato finale è che PyPy spenderebbe una frazione arbitrariamente grande del suo tempo di esecuzione nel GC – rallentando l’esecuzione effettiva, non del 10% o del 100% o del 1000% ma essenzialmente di qualsiasi fattore.

Al meglio delle nostre conoscenze questo problema non ha una soluzione migliore della correzione dei programmi. Se si verifica nel codice di terze parti, questo significa andare dagli autori e spiegare loro il problema: devono chiudere i loro file aperti per poter essere eseguiti su qualsiasi implementazione di Python non basata su Python.

Ecco alcuni dettagli più tecnici. Questo problema riguarda il tempo preciso in cui vengono chiamati i metodi __del__, che non è affidabile o tempestivo in PyPy (né Jython né IronPython). Significa anche che i riferimenti deboli possono rimanere in vita un po’ più a lungo del previsto. Questo rende i “weak proxies” (come restituiti da weakref.proxy()) un po’ meno utili: sembreranno rimanere vivi per un po’ più a lungo in PyPy, e improvvisamente saranno davvero morti, sollevando un ReferenceError al prossimo accesso. Qualsiasi codice che utilizzi proxy deboli deve attentamente catturare tali ReferenceError in qualsiasi posto che li utilizzi. (O, meglio ancora, non usare affattoweakref.proxy(); usare weakref.ref().)

Nota un dettaglio nella documentazione per i callback weakref:

Se viene fornito callback e non None, e l’oggetto weakref restituito è ancora vivo, il callback sarà chiamato quando l’oggetto sta per essere finalizzato.

Ci sono casi in cui, a causa della semantica refcount di CPython, un weakref muore immediatamente prima o dopo gli oggetti a cui punta (tipicamente con qualche riferimento circolare). Se gli capita di morire subito dopo, la callback sarà invocata. In un caso simile in PyPy, sia l’oggetto che il weakref saranno considerati morti allo stesso tempo, e la callback non sarà invocata. (Problema #2030)

Ci sono alcune implicazioni extra dalla differenza nel GC. In particolare, se un oggetto ha un __del__, il __del__ non viene mai chiamato più di una volta in PyPy; ma CPython chiamerà lo stesso __del__ più volte se l’oggetto viene resuscitato e muore di nuovo (almeno è così in modo affidabile nei CPython più vecchi; i CPython più recenti cercano di chiamare i distruttori non più di una volta, ma ci sono controesempi). I metodi __del__ sono chiamati nell’ordine “giusto” se sono su oggetti che puntano l’uno all’altro, come in CPython, ma a differenza di CPython, se c’è un ciclo morto di oggetti che si referenziano a vicenda, i loro metodi __del__ sono chiamati comunque; CPython li metterebbe invece nella lista garbage del modulo gc. Maggiori informazioni sono disponibili sul blog .

Nota che questa differenza potrebbe mostrarsi indirettamente in alcuni casi. Per esempio, un generatore lasciato in sospeso a metà è – di nuovo – raccolto più tardi in PyPy che in CPython. Si può vedere la differenza se la parola chiave yield a cui è sospeso è essa stessa racchiusa in un blocco try: o with:. Questo si presenta per esempio come problema 736.

Utilizzando il GC di default (chiamato minimark), la funzione integrata id() funziona come in CPython. Con altri GC restituisce numeri che non sono indirizzi reali (perché un oggetto può muoversi più volte) e chiamarla spesso può portare a problemi di prestazioni.

Nota che se hai una lunga catena di oggetti, ognuno con un riferimento al successivo, e ognuno con un __del__, il GC di PyPy funzionerà male. Dal lato positivo, nella maggior parte degli altri casi, i benchmark hanno dimostrato che il GC di PyPy funziona molto meglio di quello di CPython.

Un’altra differenza è che se si aggiunge un __del__ ad una classe esistente, questa non verrà chiamata:

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

Ancora più oscuro: lo stesso vale, per le classi vecchio stile, se si attacca il __del__ ad un’istanza (anche in CPython questo non funziona con le classi nuovo stile). Si ottiene un RuntimeWarning in PyPy. Per risolvere questi casi basta assicurarsi che ci sia un metodo __del__ nella classe per iniziare (anche contenente solo pass; sostituirlo o sovrascriverlo in seguito funziona bene).

Ultima nota: CPython cerca di fare un gc.collect() automaticamente quando il programma finisce; non PyPy. (È possibile sia in CPython che in PyPy progettare un caso in cui diversi gc.collect() sono necessari prima che tutti gli oggetti muoiano. Questo fa sì che l’approccio di CPython funzioni solo “la maggior parte delle volte” comunque.)

.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.