lenguaje - ¿Hay alguna razón por la cual Python 3 enumera más lento que Python 2?
python wikipedia (2)
La diferencia se debe a la sustitución del tipo int
por el tipo long
. Obviamente, las operaciones con enteros largos van a ser más lentas porque las operaciones long
son más complejas.
Si fuerza a python2 a utilizar longs estableciendo cnt
en 0L
la diferencia desaparece:
$python2 -mtimeit -n5 -r2 -s"cnt=0L" "for i in range(10000000): cnt += 1L"
5 loops, best of 2: 1.1 sec per loop
$python3 -mtimeit -n5 -r2 -s"cnt=0" "for i in range(10000000): cnt += 1"
5 loops, best of 2: 686 msec per loop
$python2 -mtimeit -n5 -r2 -s"cnt=0L" "for i in xrange(10000000): cnt += 1L"
5 loops, best of 2: 714 msec per loop
Como se puede ver en mi máquina, python3.4 es más rápido que python2 usando range
y usando xrange
cuando usa long
s. El último punto de referencia con 2 xrange
de xrange
muestra que la diferencia en este caso es mínima.
No tengo Python3.3 instalado, por lo que no puedo hacer una comparación entre 3.3 y 3.4, pero hasta donde sé, no cambió nada significativo entre estas dos versiones (con respecto al range
), por lo que los tiempos deberían ser aproximadamente los mismos. Si ve una diferencia significativa, intente inspeccionar el bytecode generado utilizando el módulo dis
. Hubo un cambio en los asignadores de memoria ( PEP 445 ), pero no tengo idea de si los asignadores de memoria predeterminados se modificaron y qué consecuencias hubo en cuanto a rendimiento.
Python 3 parece ser más lento en las enumeraciones para un bucle mínimo que Python 2 por un margen significativo, que parece empeorar con las versiones más nuevas de Python 3.
Tengo Python 2.7.6, Python 3.3.3 y Python 3.4.0 instalados en mi máquina de Windows de 64 bits (Intel i7-2700K - 3.5 GHz) con las versiones de 32 bits y 64 bits instaladas de cada Python. Si bien no existe una diferencia significativa en la velocidad de ejecución entre 32 bits y 64 bits para una versión dada dentro de sus limitaciones en cuanto al acceso a la memoria, existe una diferencia muy significativa entre los diferentes niveles de versión. Dejaré que los resultados de sincronización hablen por sí mismos de la siguiente manera:
C:/**Python34_64**/python -mtimeit -n 5 -r 2 -s"cnt = 0" "for i in range(10000000): cnt += 1"
5 loops, best of 2: **900 msec** per loop
C:/**Python33_64**/python -mtimeit -n 5 -r 2 -s"cnt = 0" "for i in range(10000000): cnt += 1"
5 loops, best of 2: **820 msec** per loop
C:/**Python27_64**/python -mtimeit -n 5 -r 2 -s"cnt = 0" "for i in range(10000000): cnt += 1"
5 loops, best of 2: **480 msec** per loop
Como el "rango" de Python 3 no es el mismo que el "rango" de Python 2, y es funcionalmente el mismo que el "xrange" de Python 2, también lo cronometré de la siguiente manera:
C:/**Python27_64**/python -mtimeit -n 5 -r 2 -s"cnt = 0" "for i in **xrange**(10000000): cnt += 1"
5 loops, best of 2: **320 msec** per loop
Uno puede ver fácilmente que la versión 3.3 es casi dos veces más lenta que la versión 2.7 y Python 3.4 es aproximadamente un 10% más lenta que esa otra vez.
Mi pregunta: ¿Hay alguna opción de entorno o configuración que corrija esto, o es solo un código ineficiente o el intérprete está haciendo más por la versión de Python 3?
La respuesta parece ser que Python 3 usa los enteros de "precisión infinita" que solía llamarse "largo" en Python 2.x su tipo "int" predeterminado sin ninguna opción para usar el bit de longitud fija "int" de Python 2 y es el procesamiento de estos "int" de longitud variable que está tomando el tiempo extra como se discutió en las respuestas y comentarios a continuación.
Puede ser que Python 3.4 sea algo más lento que Python 3.3 debido a cambios en la asignación de memoria para admitir la sincronización que desacelera levemente la asignación / desasignación de memoria, que probablemente sea la razón principal por la cual la versión actual del procesamiento "largo" se ejecuta más despacio.
Una respuesta resumida de lo que he aprendido de esta pregunta podría ayudar a otros que se preguntan lo mismo que yo:
El motivo de la ralentización es que todas las variables enteras en Python 3.x son ahora de "precisión infinita" como el tipo que se solía llamar "largo" en Python 2.x pero ahora es el único tipo de entero que decide PEP 237 . Según ese documento, los enteros "cortos" que tenían la profundidad de bit de la arquitectura base ya no existen (o solo internamente).
Las antiguas operaciones variables "cortas" podían ejecutarse razonablemente rápido porque podían usar las operaciones subyacentes del código máquina directamente y optimizar la asignación de nuevos objetos "int" porque siempre tenían el mismo tamaño.
El tipo "largo" actualmente solo está representado por un objeto de clase asignado en la memoria, ya que podría exceder una profundidad de bits dada de una ubicación de registro / memoria de longitud fija. dado que estas representaciones de objetos podrían crecer o reducirse para varias operaciones y, por lo tanto, tener un tamaño variable, no se les puede asignar una asignación de memoria fija y se pueden dejar allí.
Estos tipos "largos" (actualmente) no usan un tamaño de palabra de arquitectura de máquina completo, pero reservan un bit (normalmente el bit de signo) para realizar comprobaciones de desbordamiento, por lo que la "precisión infinita larga" se divide (actualmente) en 15 bits / "Dígitos" de corte de 30 bits para arquitecturas de 32 bits / 64 bits, respectivamente.
Muchos de los usos comunes de estos enteros "largos" no requerirán más de uno (o tal vez dos para arquitecturas de 32 bits) "dígitos" ya que el rango de un "dígito" es de aproximadamente mil millones / 32768 para 64 bits / Arquitecturas de 32 bits, respectivamente.
El código ''C'' es bastante eficiente para hacer una o dos operaciones de "dígito", por lo que el costo de rendimiento sobre los enteros "cortos" más simples no es tan alto para muchos usos comunes en lo que respecta al cálculo real en comparación con el tiempo requerido para ejecutar el bucle de intérprete de código de bytes.
El mayor golpe de rendimiento es probablemente las asignaciones / desasignaciones de memoria constantes , un par para cada entero entero de bucle, que es bastante caro, especialmente cuando Python se mueve para admitir multi-threading con bloqueos de sincronización (que es por qué Python 3.4 es peor que 3.3).
Actualmente, la implementación siempre garantiza suficientes "dígitos" al asignar un "dígito" adicional por encima del tamaño real de "dígitos" utilizados para el operando más grande si existe la posibilidad de que pueda "crecer", hacer la operación (que puede o puede en realidad no usa ese "dígito" extra), y luego normaliza la duración del resultado para dar cuenta de la cantidad real de "dígitos" utilizados, que en realidad pueden permanecer igual (o posiblemente "reducir" para algunas operaciones); esto se hace simplemente reduciendo el recuento de tamaño en la estructura "larga" sin una nueva asignación, por lo que puede desperdiciar un "dígito" de espacio de memoria, pero se ahorra el costo de rendimiento de otro ciclo de asignación / desasignación.
Existe la esperanza de una mejora en el rendimiento: para muchas operaciones, es posible predecir si la operación causará un "crecimiento" o no; por ejemplo, para una adición, solo hay que observar los bits más significativos (MSB) y la operación no puede crecer si ambas MSB son cero , lo que será el caso para muchas operaciones de bucle / contador; una resta no "crecerá" según los signos y las MSB de los dos operandos; un cambio a la izquierda solo "crecerá" si el MSB es uno; etcétera.
Para aquellos casos en que la declaración es algo así como "cnt + = 1" / "i + = paso" y así sucesivamente (abriendo la posibilidad de operaciones en el lugar para muchos casos de uso), una versión "in situ" de las operaciones podría ser llamado, que haría las comprobaciones rápidas apropiadas y solo asignaría un nuevo objeto si fuera necesario un "crecimiento"; de lo contrario, haría la operación en lugar del primer operando. La complicación sería que el compilador necesitaría producir estos códigos de bytes "in situ", sin embargo, eso ya se ha hecho , con los códigos de byte de "operación en el lugar" especiales apropiados producidos, solo que el intérprete actual de códigos de bytes dirige a la versión habitual como se describió anteriormente porque aún no se han implementado (valores cero / nulos en la tabla).
Es muy posible que todo lo que deba hacerse sea escribir versiones de estas "operaciones in situ" y completarlas en la tabla de métodos "largos" con el intérprete de códigos de bytes que ya las encuentre y las ejecute si existen o cambios menores en una tabla para hacer que los llame siendo todo lo que se requiere.
Tenga en cuenta que los flotantes son siempre del mismo tamaño, por lo que podrían realizarse las mismas mejoras, aunque los flotantes se asignan en bloques de ubicaciones de repuesto para una mejor eficiencia; sería mucho más difícil hacer eso por "largos" ya que toman una cantidad variable de memoria.
También tenga en cuenta que esto rompería la inmutabilidad de los "largos" (y opcionalmente flotantes), por lo que no hay operadores en el lugar definidos, pero el hecho de que se los trate como mutables solo para estos casos especiales no afecta el exterior mundo ya que nunca se daría cuenta de que a veces un objeto dado tiene la misma dirección que el valor anterior (siempre y cuando las comparaciones de igualdad miren los contenidos y no solo las direcciones de los objetos).
Creo que al evitar la asignación / deslocalización de memoria para estos casos de uso comunes, el rendimiento de Python 3.x será bastante similar al de Python 2.7.
Mucho de lo que he aprendido aquí proviene del archivo fuente "C" del tronco de Python para el objeto "largo"
EDIT_ADD: Whoops, se olvidó de que si las variables son a veces mutables, los cierres en las variables locales no funcionan o no funcionan sin grandes cambios, lo que significa que las operaciones en el sitio "romperían" los cierres. Parecería que una solución mejor sería obtener una asignación de reserva anticipada que funcione a "largo" como lo hacía para los enteros cortos y para los flotantes, incluso si solo para los casos en que el tamaño "largo" no cambio (que cubre la mayor parte del tiempo, como bucles y contadores según la pregunta). Hacer esto debería significar que el código no se ejecuta mucho más lento que Python 2 para un uso típico.