¿Por qué es x**4.0
más rápido que x**4
en Python 3 * ?
Los int
objetos de Python 3 son un objeto completo diseñado para admitir un tamaño arbitrario; debido a ese hecho, se manejan como tales en el nivel C (vea cómo se declaran todas las variables como PyLongObject *
tipo long_pow
). Esto también hace que su exponenciación sea mucho más complicada y tediosa, ya que debe jugar con la ob_digit
matriz que utiliza para representar su valor para realizarla. ( Fuente para los valientes. - Ver: Entender la asignación de memoria para enteros grandes en Python para más información sobre PyLongObject
s.)
Los float
objetos de Python , por el contrario, se pueden transformar a un double
tipo C (mediante el uso PyFloat_AsDouble
) y las operaciones se pueden realizar utilizando esos tipos nativos . Esto es genial porque, después de verificar los casos relevantes, le permite a Python usar las plataformaspow
( C pow
, es decir ) para manejar la exponenciación real:
/* Now iv and iw are finite, iw is nonzero, and iv is
* positive and not equal to 1.0. We finally allow
* the platform pow to step in and do the rest.
*/
errno = 0;
PyFPE_START_PROTECT("pow", return NULL)
ix = pow(iv, iw);
donde iv
y iw
son nuestros PyFloatObject
s originales como C double
s.
Por lo que vale: Python 2.7.13
para mí es un factor 2~3
más rápido y muestra el comportamiento inverso.
El hecho anterior también explica la discrepancia entre Python 2 y 3, por lo que pensé en abordar este comentario también porque es interesante.
En Python 2, está utilizando el int
objeto antiguo que difiere del int
objeto en Python 3 (todos los int
objetos en 3.x son de PyLongObject
tipo). En Python 2, hay una distinción que depende del valor del objeto (o, si usa el sufijo L/l
):
# Python 2
type(30) # <type 'int'>
type(30L) # <type 'long'>
Lo <type 'int'>
que ves aquí hace lo mismo que float
hace , se convierte de forma segura en una C long
cuando se realiza la exponenciación ( int_pow
también sugiere que el compilador los ponga en un registro si puede hacerlo, para que eso pueda marcar la diferencia) :
static PyObject *
int_pow(PyIntObject *v, PyIntObject *w, PyIntObject *z)
{
register long iv, iw, iz=0, ix, temp, prev;
/* Snipped for brevity */
Esto permite una buena ganancia de velocidad.
Para ver cuán lentos <type 'long'>
son los s en comparación con <type 'int'>
s, si envuelve el x
nombre en una long
llamada en Python 2 (esencialmente forzándolo a usarlo long_pow
como en Python 3), la ganancia de velocidad desaparece:
# <type 'int'>
(python2) ➜ python -m timeit "for x in range(1000):" " x**2"
10000 loops, best of 3: 116 usec per loop
# <type 'long'>
(python2) ➜ python -m timeit "for x in range(1000):" " long(x)**2"
100 loops, best of 3: 2.12 msec per loop
Tomar nota de que, a pesar de las transformaciones de un fragmento de las int
a long
, mientras que el otro no (como en punta a cabo por @pydsinger), este reparto no es la fuerza que contribuye detrás de la desaceleración. La implementación de long_pow
es. (Mida las declaraciones únicamente long(x)
para ver).
[...] no sucede fuera del circuito. [...] ¿Alguna idea sobre eso?
Este es el optimizador de mirilla de CPython que dobla las constantes por usted. Obtiene los mismos tiempos exactos en cualquier caso, ya que no hay un cálculo real para encontrar el resultado de la exponenciación, solo carga de valores:
dis.dis(compile('4 ** 4', '', 'exec'))
1 0 LOAD_CONST 2 (256)
3 POP_TOP
4 LOAD_CONST 1 (None)
7 RETURN_VALUE
Se genera un código de bytes idéntico '4 ** 4.'
con la única diferencia de que LOAD_CONST
carga el flotante en 256.0
lugar de int 256
:
dis.dis(compile('4 ** 4.', '', 'exec'))
1 0 LOAD_CONST 3 (256.0)
2 POP_TOP
4 LOAD_CONST 2 (None)
6 RETURN_VALUE
Entonces los tiempos son idénticos.
* Todo lo anterior se aplica únicamente a CPython, la implementación de referencia de Python. Otras implementaciones pueden funcionar de manera diferente.