Diferencias entre PyPy y CPython¶

Diferencias relacionadas con las estrategias de recolección de basura¶

Los recolectores de basura utilizados o implementados por PyPy no se basan en el conteo de referencias, por lo que los objetos no se liberan instantáneamente cuando dejan de ser alcanzables. El efecto más obvio de esto es que los archivos (y sockets, etc) no se cierran rápidamente cuando salen del ámbito. En el caso de los archivos que se abren para escribir, los datos pueden permanecer en sus búferes de salida durante un tiempo, haciendo que el archivo en el disco aparezca vacío o truncado. Además, podría alcanzar el límite de su SO en el número de archivos abiertos simultáneamente.

Si está depurando un caso en el que un archivo en su programa no se cierra correctamente, puede utilizar la opción de línea de comandos -X track-resources. Si se da, se produce un ResourceWarning para cada archivo y socket que el recolector de basura cierra. La advertencia contendrá la traza de la pila de la posición en la que se creó el archivo o el socket, para que sea más fácil ver qué partes del programa no cierran los archivos explícitamente.

Corregir esta diferencia en CPython es esencialmente imposible sin forzar un enfoque de recuento de diferencias para la recolección de basura. El efecto que se obtiene en CPython se ha descrito claramente como un efecto secundario de la implementación y no una decisión de diseño del lenguaje: los programas que dependen de esto son básicamente falsos. Sería una restricción demasiado fuerte intentar imponer el comportamiento de CPython en una especificación del lenguaje, dado que no tiene ninguna posibilidad de ser adoptado por Jython o IronPython (o cualquier otro puerto de Python a Java o.NET).

Incluso la idea ingenua de forzar una GC completa cuando nos estamos acercando peligrosamente al límite del SO puede ser muy mala en algunos casos. Si tu programa filtra mucho los archivos abiertos, entonces funcionaría, pero forzaría un ciclo GC completo cada n’s archivos filtrados. El valor de n es una constante, pero el programa puede tomar una cantidad arbitraria de memoria, lo que hace que un ciclo GC completo sea arbitrariamente largo. El resultado final es que PyPy pasaría una fracción arbitrariamente grande de su tiempo de ejecución en la GC – ralentizando la ejecución real, no en un 10% ni en un 100% ni en un 1000%, sino esencialmente en cualquier factor.

Hasta donde sabemos, este problema no tiene mejor solución que arreglar los programas. Si se produce en el código de terceros, esto significa ir a los autores y explicarles el problema: tienen que cerrar sus archivos abiertos con el fin de ejecutar en cualquier implementación de Python que no esté basada en Cython.

Aquí hay algunos detalles más técnicos. Este problema afecta al momento preciso en el que se llaman los métodos __del__, que no es fiable ni oportuno en PyPy (ni en Jython ni en IronPython). También significa que las referencias débiles pueden permanecer vivas durante un poco más de lo esperado. Esto hace que los «proxies débiles» (devueltos por weakref.proxy()) sean algo menos útiles: parecerán permanecer vivos durante un poco más de tiempo en PyPy, y de repente estarán realmente muertos, levantando un ReferenceError en el siguiente acceso. Cualquier código que utilice proxies débiles debe atrapar cuidadosamente tales ReferenceError en cualquier lugar que los utilice. (O, mejor aún, no use weakref.proxy() en absoluto; use weakref.ref().)

Observe un detalle en la documentación de las devoluciones de llamada de weakref:

Si se proporciona una devolución de llamada y no None, y el objeto weakref devuelto sigue vivo, la devolución de llamada será llamada cuando el objeto esté a punto de ser finalizado.

Hay casos en los que, debido a la semántica de refcount de CPython, una weakref muere inmediatamente antes o después de los objetos a los que apunta (típicamente con alguna referencia circular). Si ocurre que muere justo después, entonces se invocará el callback. En un caso similar en PyPy, tanto el objeto como la referencia débil se considerarán muertos al mismo tiempo, y no se invocará la devolución de llamada. (Issue #2030)

Hay algunas implicaciones adicionales de la diferencia en la GC. Principalmente, si un objeto tiene un __del__, el __del__ nunca se llama más de una vez en PyPy; pero CPython llamará al mismo __del__ varias veces si el objeto es resucitado y muere de nuevo (al menos es fiable en los CPythons más antiguos; los CPythons más nuevos intentan llamar a los destructores no más de una vez, pero hay contraejemplos). Los métodos __del__ son llamados en el orden «correcto» si están en objetos que se apuntan entre sí, como en CPython, pero a diferencia de CPython, si hay un ciclo muerto de objetos que se referencian entre sí, sus métodos __del__ son llamados de todos modos; CPython en cambio los pondría en la lista garbage del módulo gc. Más información está disponible en el blog.

Tenga en cuenta que esta diferencia podría aparecer indirectamente en algunos casos. Por ejemplo, un generador que queda pendiente en el medio es -de nuevo- recogido más tarde en PyPy que en CPython. Puedes ver la diferencia si la palabra clave yield en la que se suspende está a su vez encerrada en un bloque try: o un bloque with:. Esto aparece, por ejemplo, como número 736.

Usando la GC por defecto (llamada minimark), la función incorporada id() funciona como lo hace en CPython. Con otros GCs devuelve números que no son direcciones reales (porque un objeto puede moverse varias veces) y llamarlo mucho puede llevar a un problema de rendimiento.

Nota que si tienes una larga cadena de objetos, cada uno con una referencia al siguiente, y cada uno con un __del__, el GC de PyPy funcionará mal. En el lado positivo, en la mayoría de los otros casos, los benchmarks han mostrado que los GC de PyPy se desempeñan mucho mejor que los de CPython.

Otra diferencia es que si añades un __del__ a una clase existente no será llamada:

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

Aún más oscuro: lo mismo ocurre, para las clases de estilo antiguo, si adjuntas el __del__ a una instancia (incluso en CPython esto no funciona con clases de estilo nuevo). Se obtiene un RuntimeWarning en PyPy. Para arreglar estos casos, asegúrese de que hay un método __del__ en la clase para empezar (incluso conteniendo sólo pass; reemplazarlo o anularlo más tarde funciona bien).

Última nota: CPython intenta hacer un gc.collect() automáticamente cuando el programa termina; no PyPy. (Es posible tanto en CPython como en PyPy diseñar un caso en el que se necesiten varios gc.collect() antes de que mueran todos los objetos. Esto hace que el enfoque de CPython sólo funcione «la mayor parte del tiempo» de todos modos.)

Deja una respuesta

Tu dirección de correo electrónico no será publicada.