2009-09-08 8 views
33

que ha sido optimizar algún código Python, y probamos el siguiente experimento:¿Por qué la resta es más rápida que la suma en Python?

import time 

start = time.clock() 
x = 0 
for i in range(10000000): 
    x += 1 
end = time.clock() 

print '+=',end-start 

start = time.clock() 
x = 0 
for i in range(10000000): 
    x -= -1 
end = time.clock() 

print '-=',end-start 

El segundo bucle es fiable más rápido, en cualquier lugar de un pelo a un 10%, dependiendo del sistema de corro en. Intenté variar el orden de los bucles, el número de ejecuciones, etc., y parece que todavía funciona.

Extraño,

for i in range(10000000, 0, -1): 

(es decir, se ejecuta el bucle hacia atrás) es más rápido que

for i in range(10000000): 

incluso cuando el contenido de bucle son idénticos.

¿Qué ofrece, y hay una lección de programación más general aquí?

+4

¿Has probado esto con 'xrange()' en lugar de 'range()', por lo que Python no tiene que asignar espacio para diez millones de enteros? Además, ¿está ejecutando esto en Python 2.xo Python 3.x, que tienen implementaciones significativamente diferentes de 'range()'? –

+0

¿Es posible que ejecute este código en una CPU con frecuencia variable? – tzot

Respuesta

13
$ python -m timeit -s "x=0" "x+=1" 
10000000 loops, best of 3: 0.151 usec per loop 
$ python -m timeit -s "x=0" "x-=-1" 
10000000 loops, best of 3: 0.154 usec per loop 

Parece que has alguna measurement bias

+0

Artículo muy interesante. – hirschhornsalz

+1

Seguramente el punto de sesgo de medición es que su contraejemplo no lo prueba de una forma u otra. ¡Así que tengo un sesgo de medición! – Statto

+0

La tuya salió un poco más lenta ...;) – Pod

-1

El bucle corriendo hacia atrás es más rápido debido a que el equipo tiene un tiempo más fácil comparar si un número es igual a 0.

+1

Si bien esto puede ser cierto en el lenguaje ensamblador optimizado, generalmente * no * es cierto para lenguajes de alto nivel como Python, y en cualquier caso es poco probable que haga una diferencia tan significativa como 10%. –

+0

No hace ninguna comparación sobre el valor de x aquí? – Pod

+0

Ninguno de los bucles se está ejecutando hacia atrás. Ambos van hacia arriba, uno sumando 1 y otro restando -1. –

7

creo que la "programación general la lección "es que es realmente difícil de predecir, únicamente mirando el código fuente, qué secuencia de declaraciones será la más rápida. Los programadores en todos los niveles con frecuencia se ven atrapados por este tipo de optimización "intuitiva". Lo que crees que sabes puede no ser necesariamente cierto.

Simplemente no hay sustituto para el que mide el rendimiento de su programa. Felicitaciones por hacerlo; respondiendo por qué indudablemente requiere profundizar en la implementación de Python, en este caso.

Con idiomas compilados en bytes como Java, Python y .NET, ni siquiera es suficiente para medir el rendimiento en una sola máquina. Las diferencias entre las versiones de VM, las implementaciones de traducción de códigos nativas, las optimizaciones específicas de la CPU, etc. harán que este tipo de preguntas sea aún más difícil de responder.

0

Con Python 2.5, el mayor problema aquí es usar rango, que asignará una lista tan grande para iterar sobre ella. Al usar xrange, lo que ocurra en segundo lugar es un poco más rápido para mí. (No estoy seguro si el rango se ha convertido en generador en Python 3.)

+0

He probado con xrange (en 2.6.2), y - = 1 sigue siendo consistentemente más rápido para mí que + = 1, independientemente de cuál se haga primero. –

4

Siempre es una buena idea hacer una pregunta para decir qué plataforma y qué versión de Python está usando. A veces no importa. Este no es uno de esos momentos:

  1. time.clock() es apropiado sólo en Windows. Bote su propio código de medición y use -m timeit como se demostró en la respuesta del pixelbeat.

  2. Python 2.X range() construye una lista. Si está utilizando Python 2.x, reemplace range con xrange y vea qué sucede.

  3. Python 3.X's int es Python2.X's long.

72

Puedo reproducir esto en mi Q6600 (Python 2.6.2); aumento de la gama de 100000000:

('+=', 11.370000000000001) 
('-=', 10.769999999999998) 

primer lugar, algunas observaciones:

  • Esto es 5% para una operación trivial. Eso es significativo.
  • La velocidad de los códigos de operación de suma y resta nativos es irrelevante. Está en el piso de ruido, completamente eclipsado por la evaluación de bytecode. Eso es hablar de una o dos instrucciones nativas de miles.
  • El bytecode genera exactamente el mismo número de instrucciones; la única diferencia es INPLACE_ADD contra INPLACE_SUBTRACT y +1 contra -1.

Mirando la fuente de Python, puedo adivinar. Esto se maneja en ceval.c, en PyEval_EvalFrameEx. INPLACE_ADD tiene un bloque de código extra significativo para manejar la concatenación de cadenas. Ese bloque no existe en INPLACE_SUBTRACT, ya que no puede restar cadenas. Eso significa que INPLACE_ADD contiene más código nativo. Dependiendo (¡en gran medida!) De cómo el compilador genera el código, este código adicional puede estar en línea con el resto del código INPLACE_ADD, lo que significa que las adiciones pueden afectar al caché de instrucciones más que a la resta. Este podría estar causando hits de caché L2 adicionales, lo que podría causar una diferencia significativa en el rendimiento.

Esto depende mucho del sistema en el que se encuentre (diferentes procesadores tienen diferentes cantidades de arquitecturas de caché y caché), el compilador en uso, incluida la versión particular y las opciones de compilación (diferentes compiladores decidirán de forma diferente qué bits de código están en la ruta crítica, que determina cómo el código ensamblador se agrupa), y así sucesivamente.

Además, la diferencia se invierte en Python 3.0.1 (+: 15.66, -: 16.71); sin duda, esta función crítica ha cambiado mucho.

+0

bastante esclarecedor. +1 – Triptych

+5

Maldita sea, esa es una muy buena respuesta para no haber sido aceptado :( – mgiuca

+0

+1 gran trabajo Glenn – alex

0

Su experimento es defectuoso. La forma en que debe diseñarse este experimento es escribir 2 programas diferentes: 1 para la suma, 1 para la resta. Deben ser exactamente iguales y ejecutarse en las mismas condiciones con los datos que se archivan. Entonces necesita promediar las carreras (al menos varios miles), pero necesitaría un estadístico para decirle un número apropiado.

Si desea analizar diferentes métodos de suma, resta y bucle, una vez más, cada uno de ellos debe ser un programa separado.

El error experimental podría surgir de calor del procesador y otra actividad que va en la CPU, así que me gustaría ejecutar las carreras en una variedad de patrones ...

+0

Debería, pero no es el problema. Lo he probado de ambas formas, y obtengo los mismos resultados de cualquier manera; el caso de resta es consistentemente más rápido para mí en 2.6. –

5

"El segundo bucle es fiable más rápido .. . "

Esa es su explicación allí. Reordenar la secuencia de comandos por lo que la prueba de la resta se mide el tiempo en primer lugar, continuación la adición, y de repente Además se convierte en el funcionamiento más rápido de nuevo:

-= 3.05 
+= 2.84 

Obviamente algo le sucede a la segunda mitad del script que hace que sea más rápido.Mi suposición es que la primera llamada a range() es más lento porque pitón tiene que asignar suficiente memoria para una lista tan larga, pero es capaz de volver a utilizar esa memoria para la segunda llamada a range():

import time 
start = time.clock() 
x = range(10000000) 
end = time.clock() 
del x 
print 'first range()',end-start 
start = time.clock() 
x = range(10000000) 
end = time.clock() 
print 'second range()',end-start 

Unas pocas carreras de este script muestran que el tiempo adicional necesario para los primeros range() cuentas para casi la totalidad de la diferencia de tiempo entre '+ =' y '- =' ha visto anteriormente:

first range() 0.4 
second range() 0.23 
+0

Dupe de la respuesta de dtmunir; OP ya dijo que obtiene los mismos resultados independientemente del orden. He reproducido esto, así como cuando las dos pruebas se ejecutan de forma independiente, y cuando Se usa xrange en lugar de rango –

+0

@Glenn: Interpreté 'intentó variar el orden de los bucles' como 'rango usado (10000000, 0, -1) en vez de rango (10000000)' porque de inmediato entra en más detalles sobre el uso rango invertido(). No dice específicamente que trató de poner primero la prueba de resta. La respuesta de dtmunir tiene la misma ambigüedad, que probablemente sea la razón por la que se votó negativamente. –

+0

¡No 'de inmediato' entrar en detalles sobre el uso del rango inverso! Intenté poner el ciclo de resta primero, y también ejecutar ambas pruebas varias veces en una variedad de órdenes. – Statto

0

eso sería notable, por lo He evaluado a fondo tu código y también configuré el expiriment como me gustaría d lo encuentra más correcto (todas las declaraciones y llamadas a funciones fuera del ciclo). Ambas versiones he corrido cinco veces.

  • Ejecutando su código validado sus afirmaciones: - = toma constantemente menos tiempo; 3.6% en promedio
  • Correr mi código, sin embargo, contradice el resultado de su experimento: + = toma en promedio (no siempre) 0.5% menos de tiempo.

para mostrar todos los resultados que he puesto parcelas en línea:

Por lo tanto, concluir que el experimento tiene un sesgo, y es significativo.

Finalmente aquí está mi código:

import time 

addtimes = [0.] * 100 
subtracttimes = [0.] * 100 

range100 = range(100) 
range10000000 = range(10000000) 

j = 0 
i = 0 
x = 0 
start = 0. 


for j in range100: 
start = time.clock() 
x = 0 
for i in range10000000: 
    x += 1 
addtimes[j] = time.clock() - start 

for j in range100: 
start = time.clock() 
x = 0 
for i in range10000000: 
    x -= -1 
subtracttimes[j] = time.clock() - start 

print '+=', sum(addtimes) 
print '-=', sum(subtracttimes) 
2
¿Hay una lección más general de programación aquí?

La lección de programación más general aquí es que la intuición es una guía deficiente al predecir el rendimiento en tiempo de ejecución del código de la computadora.

Se puede razonar acerca de la complejidad algorítmica, la hipótesis sobre las optimizaciones del compilador, la estimación del rendimiento de la memoria caché, etc. Sin embargo, dado que estas cosas pueden interactuar de manera no trivial, la única manera de estar seguros de cuán rápido va a ser una pieza de código en particular es compararla en el entorno objetivo (como lo ha hecho legítimamente)

Cuestiones relacionadas