Mientras optimizaba mi código, me di cuenta de lo siguiente:
>>> from timeit import Timer as T
>>> T(lambda : 1234567890 / 4.0).repeat()
[0.22256922721862793, 0.20560789108276367, 0.20530295372009277]
>>> from __future__ import division
>>> T(lambda : 1234567890 / 4).repeat()
[0.14969301223754883, 0.14155197143554688, 0.14141488075256348]
>>> T(lambda : 1234567890 * 0.25).repeat()
[0.13619112968444824, 0.1281130313873291, 0.12830305099487305]
y también:
>>> from math import sqrt
>>> T(lambda : sqrt(1234567890)).repeat()
[0.2597470283508301, 0.2498021125793457, 0.24994492530822754]
>>> T(lambda : 1234567890 ** 0.5).repeat()
[0.15409398078918457, 0.14059877395629883, 0.14049601554870605]
Supongo que tiene que ver con la forma en que se implementa Python en C, pero me pregunto si a alguien le importaría explicar por qué es así.
Respuestas:
La razón (algo inesperada) de sus resultados es que Python parece plegar expresiones constantes que involucran multiplicación y exponenciación de punto flotante, pero no división.
math.sqrt()
es una bestia completamente diferente ya que no tiene un código de bytes e implica una llamada de función.En Python 2.6.5, el siguiente código:
x1 = 1234567890.0 / 4.0 x2 = 1234567890.0 * 0.25 x3 = 1234567890.0 ** 0.5 x4 = math.sqrt(1234567890.0)
compila con los siguientes códigos de bytes:
# x1 = 1234567890.0 / 4.0 4 0 LOAD_CONST 1 (1234567890.0) 3 LOAD_CONST 2 (4.0) 6 BINARY_DIVIDE 7 STORE_FAST 0 (x1) # x2 = 1234567890.0 * 0.25 5 10 LOAD_CONST 5 (308641972.5) 13 STORE_FAST 1 (x2) # x3 = 1234567890.0 ** 0.5 6 16 LOAD_CONST 6 (35136.418286444619) 19 STORE_FAST 2 (x3) # x4 = math.sqrt(1234567890.0) 7 22 LOAD_GLOBAL 0 (math) 25 LOAD_ATTR 1 (sqrt) 28 LOAD_CONST 1 (1234567890.0) 31 CALL_FUNCTION 1 34 STORE_FAST 3 (x4)
Como puede ver, la multiplicación y la exponenciación no toman mucho tiempo, ya que están listas cuando se compila el código. La división tarda más ya que ocurre en tiempo de ejecución. La raíz cuadrada no solo es la operación más costosa desde el punto de vista computacional de las cuatro, sino que también incurre en varios gastos generales que las otras no (búsqueda de atributos, llamada de función, etc.)
Si elimina el efecto del plegado constante, hay poco para separar la multiplicación y la división:
In [16]: x = 1234567890.0 In [17]: %timeit x / 4.0 10000000 loops, best of 3: 87.8 ns per loop In [18]: %timeit x * 0.25 10000000 loops, best of 3: 91.6 ns per loop
math.sqrt(x)
es en realidad un poco más rápido quex ** 0.5
, presumiblemente porque es un caso especial de este último y, por lo tanto, se puede hacer de manera más eficiente, a pesar de los gastos generales:In [19]: %timeit x ** 0.5 1000000 loops, best of 3: 211 ns per loop In [20]: %timeit math.sqrt(x) 10000000 loops, best of 3: 181 ns per loop
edit 2011-11-16: El plegado de expresiones constantes lo realiza el optimizador de mirilla de Python. El código fuente (
peephole.c
) contiene el siguiente comentario que explica por qué la división constante no se pliega:case BINARY_DIVIDE: /* Cannot fold this operation statically since the result can depend on the run-time presence of the -Qnew flag */ return 0;
La
-Qnew
bandera habilita la "verdadera división" definida en PEP 238 .fuente
/
Python 3, y con//
Python 2 y 3. Así que lo más probable es que esto sea el resultado del hecho de que/
puede tener diferentes significados en Python 2. Tal vez cuando se realiza el plegado constante, aún no se sabe sifrom __future__ import division
es ¿en efecto?1./0.
en Python 2.7 no da como resultadoNaN
unZeroDivisionError
.