¿Por qué es x ** 4.0 más rápido que x ** 4 en Python 3?

164

¿Por qué es x**4.0más rápido que x**4? Estoy usando CPython 3.5.2.

$ python -m timeit "for x in range(100):" " x**4.0"
  10000 loops, best of 3: 24.2 usec per loop

$ python -m timeit "for x in range(100):" " x**4"
  10000 loops, best of 3: 30.6 usec per loop

Intenté cambiar la potencia con la que aumenté para ver cómo actúa, y por ejemplo, si elevo x a la potencia de 10 o 16, está saltando de 30 a 35, pero si estoy aumentando 10.0 como flotador, solo se mueve alrededor de 24.1 ~ 4.

Supongo que tiene algo que ver con la conversión de flotación y potencias de 2 tal vez, pero realmente no lo sé.

Noté que en ambos casos las potencias de 2 son más rápidas, supongo que dado que esos cálculos son más nativos / fáciles para el intérprete / computadora. Pero aún así, con las carrozas casi no se mueve. 2.0 => 24.1~4 & 128.0 => 24.1~4 pero 2 => 29 & 128 => 62


TigerhawkT3 señaló que no sucede fuera del circuito. Verifiqué y la situación solo ocurre (por lo que he visto) cuando la base se está elevando. ¿Alguna idea sobre eso?

arieljannai
fuente
11
Para lo que vale: Python 2.7.13 para mí es un factor 2 ~ 3 más rápido, y muestra el comportamiento inverso: un exponente entero es más rápido que un exponente de coma flotante.
44
@Evert sí, obtuve 14 usec para x**4.0y 3.9 para x**4.
dabadaba

Respuestas:

161

¿Por qué es x**4.0 más rápido que x**4en Python 3 * ?

Los intobjetos 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_digitmatriz 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 PyLongObjects.)

Los floatobjetos de Python , por el contrario, se pueden transformar a un doubletipo 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 ivy iwson nuestros PyFloatObjects originales como C doubles.

Por lo que vale: Python 2.7.13para mí es un factor 2~3má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 intobjeto antiguo que difiere del intobjeto en Python 3 (todos los intobjetos en 3.x son de PyLongObjecttipo). 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 floathace , se convierte de forma segura en una C long cuando se realiza la exponenciación ( int_powtambié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 xnombre en una longllamada en Python 2 (esencialmente forzándolo a usarlo long_powcomo 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 inta 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_powes. (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_CONSTcarga el flotante en 256.0lugar 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.

Dimitris Fasarakis Hilliard
fuente
Sea lo que sea, está relacionado con el bucle sobre a range, ya que la sincronización de la **operación en sí no produce diferencias entre enteros y flotantes.
TigerhawkT3
La diferencia solo aparece cuando se busca una variable ( 4**4es tan rápido como 4**4.0), y esta respuesta no toca eso en absoluto.
TigerhawkT3
1
Pero, las constantes se doblarán en @ TigerhawkT3 ( dis(compile('4 ** 4', '', 'exec'))), por lo que el tiempo debe ser exactamente el mismo.
Dimitris Fasarakis Hilliard
Tus últimos tiempos parecen no mostrar lo que dices. long(x)**2.sigue siendo más rápido que long(x)**2por un factor de 4-5. (Sin embargo
ninguno de los votantes negativos
3
@ mbomb007 la eliminación del <type 'long'>tipo en Python 3 probablemente se explica por los esfuerzos realizados para simplificar el lenguaje. Si puede tener un tipo para representar enteros, es más manejable que dos (y preocuparse por la conversión de uno a otro cuando sea necesario, los usuarios se confunden, etc.). La ganancia de velocidad es secundaria a eso. La sección de fundamentos de PEP 237 también ofrece más información.
Dimitris Fasarakis Hilliard
25

Si observamos el código de bytes, podemos ver que las expresiones son puramente idénticas. La única diferencia es un tipo de constante que será un argumento de BINARY_POWER. Por lo tanto, sin duda se debe a un intser convertido a un número de coma flotante en la línea.

>>> def func(n):
...    return n**4
... 
>>> def func1(n):
...    return n**4.0
... 
>>> from dis import dis
>>> dis(func)
  2           0 LOAD_FAST                0 (n)
              3 LOAD_CONST               1 (4)
              6 BINARY_POWER
              7 RETURN_VALUE
>>> dis(func1)
  2           0 LOAD_FAST                0 (n)
              3 LOAD_CONST               1 (4.0)
              6 BINARY_POWER
              7 RETURN_VALUE

Actualización: echemos un vistazo a Objects / abstract.c en el código fuente de CPython:

PyObject *
PyNumber_Power(PyObject *v, PyObject *w, PyObject *z)
{
    return ternary_op(v, w, z, NB_SLOT(nb_power), "** or pow()");
}

PyNumber_Powerllamadas ternary_op, que es demasiado largo para pegar aquí, así que aquí está el enlace .

Llama a la nb_powerranura de x, pasando ycomo argumento.

Finalmente, en la float_pow()línea 686 de Objects / floatobject.c vemos que los argumentos se convierten a C doublejusto antes de la operación real:

static PyObject *
float_pow(PyObject *v, PyObject *w, PyObject *z)
{
    double iv, iw, ix;
    int negate_result = 0;

    if ((PyObject *)z != Py_None) {
        PyErr_SetString(PyExc_TypeError, "pow() 3rd argument not "
            "allowed unless all arguments are integers");
        return NULL;
    }

    CONVERT_TO_DOUBLE(v, iv);
    CONVERT_TO_DOUBLE(w, iw);
    ...
leovp
fuente
1
@ Jean-FrançoisFabre Creo que se debe al doblez constante.
Dimitris Fasarakis Hilliard
2
Creo que la implicación de que hay una conversión y que no se manejan de manera diferente "seguramente" es un poco exagerado sin una fuente.
miradulo
1
@Mitch: especialmente porque, en este código en particular, no hay diferencia en el tiempo de ejecución de esas dos operaciones. La diferencia solo surge con el bucle del OP. Esta respuesta está llegando a conclusiones.
TigerhawkT3
2
¿Por qué solo float_powmiras cuando eso ni siquiera funciona para el caso lento?
user2357112 es compatible con Monica
2
@ TigerhawkT3: 4**4y 4**4.0se dobla constantemente. Ese es un efecto completamente separado.
user2357112 es compatible con Monica
-1

Porque uno es correcto, otro es aproximación.

>>> 334453647687345435634784453567231654765 ** 4.0
1.2512490121794596e+154
>>> 334453647687345435634784453567231654765 ** 4
125124901217945966595797084130108863452053981325370920366144
719991392270482919860036990488994139314813986665699000071678
41534843695972182197917378267300625
Veky
fuente
No sé por qué ese votante negativo votó negativamente, pero lo hice porque esta respuesta no responde la pregunta. El hecho de que algo sea correcto no implica de ninguna manera que sea más rápido o más lento. Uno es más lento que el otro porque uno puede trabajar con tipos C mientras que el otro tiene que trabajar con Python Objects.
Dimitris Fasarakis Hilliard
1
Gracias por la explicación. Bueno, realmente pensé que era obvio que es más rápido calcular solo la aproximación de un número a 12 dígitos más o menos, que calcularlos todos exactamente. Después de todo, la única razón por la que usamos aproximaciones es que son más rápidas de calcular, ¿verdad?
Veky