Rendimiento de los recursos de la biblioteca en comparación con el código python

Como respuesta a mi pregunta Encuentra la posición basada en 1 en la que dos listas son las mismas , obtuve la sugerencia de usar las herramientas de la biblioteca C para acelerar las cosas.

Para verificar codifiqué la siguiente prueba usando cProfile:

from itertools import takewhile, izip def match_iter(self, other): return sum(1 for x in takewhile(lambda x: x[0] == x[1], izip(self, other))) def match_loop(self, other): element = -1 for element in range(min(len(self), len(other))): if self[element] != other[element]: element -= 1 break return element +1 def test(): a = [0, 1, 2, 3, 4] b = [0, 1, 2, 3, 4, 0] print("match_loop a=%s, b=%s, result=%s" % (a, b, match_loop(a, b))) print("match_iter a=%s, b=%s, result=%s" % (a, b, match_iter(a, b))) i = 10000 while i > 0: i -= 1 match_loop(a, b) match_iter(a, b) def profile_test(): import cProfile cProfile.run('test()') if __name__ == '__main__': profile_test() 

La función match_iter () está usando los itertools y la función match_loop () es la que implementé antes de usar python plano.

La función test () define dos listas, imprime las listas con los resultados de las dos funciones para verificar que está funcionando. Ambos resultados tienen el valor esperado 5, que es la longitud para que las listas sean iguales. Luego se repite 10.000 veces en ambas funciones.

Finalmente, todo se perfila utilizando profile_test ().

Lo que aprendí fue que izip no está implementado en los itertools de python3, al menos no en debian wheezy whitch estoy usando. Así que había corrido la prueba con python2.7

Aquí están los resultados:

 python2.7 match_test.py match_loop a=[0, 1, 2, 3, 4], b=[0, 1, 2, 3, 4, 0], result=5 match_iter a=[0, 1, 2, 3, 4], b=[0, 1, 2, 3, 4, 0], result=5 180021 function calls in 0.636 seconds Ordered by: standard name ncalls tottime percall cumtime percall filename:lineno(function) 1 0.000 0.000 0.636 0.636 :1() 1 0.039 0.039 0.636 0.636 match_test.py:15(test) 10001 0.048 0.000 0.434 0.000 match_test.py:3(match_iter) 60006 0.188 0.000 0.275 0.000 match_test.py:4() 50005 0.087 0.000 0.087 0.000 match_test.py:4() 10001 0.099 0.000 0.162 0.000 match_test.py:7(match_loop) 20002 0.028 0.000 0.028 0.000 {len} 1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects} 10001 0.018 0.000 0.018 0.000 {min} 10001 0.018 0.000 0.018 0.000 {range} 10001 0.111 0.000 0.387 0.000 {sum} 

Lo que me hace preguntarme es que, al observar los valores de tiempo de ejecución, mi versión de python simple tiene un valor de 0.162 segundos para 10,000 bucles y la versión match_iter toma 0.434 segundos.

Por un lado, Python es muy rápido, genial, así que no tengo que preocuparme. ¿Pero puede ser correcto, que la biblioteca C demore más del doble de tiempo en terminar el trabajo como un simple código de Python? ¿O estoy cometiendo un error fatal?

Para verificar, también realicé la prueba con python2.6, que parece ser incluso más rápido, pero con la misma diferencia entre looping e itertools.

¿Quién tiene experiencia y está dispuesto a ayudar?

Me imagino que el problema aquí es que sus listas de pruebas son pequeñas, lo que significa que es probable que cualquier diferencia sea mínima, y ​​que el costo de crear los iteradores supera las ganancias que dan.

En pruebas más grandes (donde el rendimiento es más probable que importe), la versión que usa sum() probablemente superará a la otra versión.

Además, está la cuestión del estilo: la versión manual es más larga y se basa en la iteración por índice, lo que la hace menos flexible también.

Yo diría que la solución más legible sería algo como esto:

 def while_equal(seq, other): for this, that in zip(seq, other): if this != that: return yield this def match(seq, other): return sum(1 for _ in while_equal(seq, other)) 

Curiosamente, en mi sistema una versión ligeramente modificada de esto:

 def while_equal(seq, other): for this, that in zip(seq, other): if this != that: return yield 1 def match(seq, other): return sum(while_equal(seq, other)) 

Funciona mejor que la versión de bucle puro:

 a = [0, 1, 2, 3, 4] b = [0, 1, 2, 3, 4, 0] import timeit print(timeit.timeit('match_loop(a,b)', 'from __main__ import a, b, match_loop')) print(timeit.timeit('match(a,b)', 'from __main__ import match, a, b')) 

Dando

 1.3171300539979711 1.291257290984504 

Dicho esto, si mejoramos la versión de loop puro para que sea más Pythonic:

 def match_loop(seq, other): count = 0 for this, that in zip(seq, other): if this != that: return count count += 1 return count 

Este tiempo (usando el mismo método anterior) en 0.8548871780512854 para mí, significativamente más rápido que cualquier otro método, mientras que todavía es legible. Probablemente, esto se debe a que la versión original hace un bucle por índice, que generalmente es muy lento. Sin embargo, me gustaría ir a la primera versión de este post, ya que creo que es la más legible.

  • Primero, felicitaciones por realmente sincronizar algo.
  • Segundo, la legibilidad es generalmente más importante que escribir código rápido. Si su código se ejecuta 3 veces más rápido, pero gasta 2 de cada 3 semanas en depurarlo, no vale la pena dedicarle tiempo.
  • Tercero, también puede usar timeit para timeit pequeños bits de código. Encuentro que este enfoque es un poco más fácil que usar el profile . (Aunque el profile es bueno para encontrar cuellos de botella).

itertools es, en general, bastante rápido. Sin embargo, especialmente en este caso, su takewhile irá ralentizando las cosas porque itertools necesita activar una función para cada elemento en el camino. Cada llamada a la función en Python tiene una cantidad razonable de sobrecarga asociada, por lo que podría ralentizarte un poco (también existe el costo de crear la función lambda en primer lugar). Tenga en cuenta que la sum con la expresión del generador también agrega un poco de sobrecarga. En última instancia, sin embargo, parece que un bucle básico gana en esta situación todo el tiempo.

 from itertools import takewhile, izip def match_iter(self, other): return sum(1 for x in takewhile(lambda x: x[0] == x[1], izip(self, other))) def match_loop(self, other): cmp = lambda x1,x2: x1 == x2 for element in range(min(len(self), len(other))): if self[element] == other[element]: element += 1 else: break return element def match_loop_lambda(self, other): cmp = lambda x1,x2: x1 == x2 for element in range(min(len(self), len(other))): if cmp(self[element],other[element]): element += 1 else: break return element def match_iter_nosum(self,other): element = 0 for _ in takewhile(lambda x: x[0] == x[1], izip(self, other)): element += 1 return element def match_iter_izip(self,other): element = 0 for x1,x2 in izip(self,other): if x1 == x2: element += 1 else: break return element a = [0, 1, 2, 3, 4] b = [0, 1, 2, 3, 4, 0] import timeit print timeit.timeit('match_iter(a,b)','from __main__ import a,b,match_iter') print timeit.timeit('match_loop(a,b)','from __main__ import a,b,match_loop') print timeit.timeit('match_loop_lambda(a,b)','from __main__ import a,b,match_loop_lambda') print timeit.timeit('match_iter_nosum(a,b)','from __main__ import a,b,match_iter_nosum') print timeit.timeit('match_iter_izip(a,b)','from __main__ import a,b,match_iter_izip') 

Sin embargo, tenga en cuenta que la versión más rápida es un híbrido de un bucle + itertools. Este bucle (explícito) sobre izip también resulta ser más fácil de leer (en mi opinión). Por lo tanto, podemos concluir a partir de esto que la takewhile es la parte lenta, no necesariamente el itertools en general.