2012-01-20 13 views
5

Me encuentro con lo que parece ser una sorprendente diferencia de rendimiento al iterar sobre un contenedor pequeño con un iterador personalizado. Esperaba que alguien pudiera ayudarme a entender de dónde vienen estas diferencias.rendimiento del iterador personalizado

Primero algo de contexto; Estoy escribiendo varios módulos de extensión de Python usando boost :: python, uno de los cuales contiene un enlace a un tipo de vector flotante 3d que implementa getitem. Como tiene getitem es posible iterar sobre él, sin embargo, parece bastante lento, pero no es obvio por qué, así que decidí jugar con algunos iteradores personalizados simples en Python para tener una mejor idea de cómo funcionan las cosas. Que es donde estos iteradores vinieron de ...

class MyIterator1(object): 
    __slots__ = ['values', 'popfn'] 

    def __init__(self): 
     self.values = ['x', 'y', 'z'] 
     self.popfn = self.values.pop 

    def __length_hint__(self): 
     return 3 

    def __iter__(self): 
     return self 

    def next(self): 
     try: 
      return self.popfn() 
     except IndexError: 
      raise StopIteration 

class MyIterator2(object): 
    __slots__ = ['values', 'itfn'] 

    def __init__(self): 
     self.values = ['x', 'y', 'z'] 
     it = iter(self.values) 
     self.itfn = it.next 

    def __length_hint__(self): 
     return 3 

    def __iter__(self): 
     return self 

    def next(self): 
     return self.itfn() 

class MyIterator3(object): 
    __slots__ = ['values', 'i'] 

    def __init__(self): 
     self.values = ['x', 'y', 'z'] 
     self.i = 0 

    def __length_hint__(self): 
     return 3 

    def __iter__(self): 
     return self 

    def next(self): 
     if self.i >= 3: 
      raise StopIteration 
     value = self.values[self.i] 
     self.i += 1 
     return value 

def MyIterator4(): 
    val = ['x', 'y', 'z'] 
    yield val[0] 
    yield val[1] 
    yield val[2] 

siguiente me encontré con estos a través de una secuencia de comandos con el módulo timeit (que supone el código anterior se encuentra en un módulo llamado testiter)

import timeit 

timer1 = timeit.Timer('r = list(testiter.MyIterator1())', 'import testiter') 
timer2 = timeit.Timer('r = list(testiter.MyIterator2())', 'import testiter') 
timer3 = timeit.Timer('r = list(testiter.MyIterator3())', 'import testiter') 
timer4 = timeit.Timer('r = list(testiter.MyIterator4())', 'import testiter') 
timer5 = timeit.Timer('r = list(iter(["x", "y", "z"]))', 'import testiter') 

print 'list(testiter.MyIterator1())' 
print timer1.timeit() 

print "\n" 

print 'list(testiter.MyIterator2())' 
print timer2.timeit() 

print "\n" 

print 'list(testiter.MyIterator3())' 
print timer3.timeit() 

print "\n" 

print 'list(testiter.MyIterator4())' 
print timer4.timeit() 

print "\n" 

print 'list(iter(["x", "y", "z"]))' 
print timer5.timeit() 

Esta impresora fuera de la siguiente

list(testiter.MyIterator1()) 
8.5735929


list(testiter.MyIterator2()) 
5.28959393501 


list(testiter.MyIterator3()) 
6.11230111122 


list(testiter.MyIterator4()) 
2.31263613701 


list(iter(["x", "y", "z"])) 
1.26243281364 

Como era de esperar el catalogador python es el más rápido, por un buen margen. Supongo que esto se debe a algunas optimizaciones mágicas dentro de Python. El generador también es considerablemente más rápido que las clases de MyIterator, lo que de nuevo no me sorprende enormemente, y supongo que se debe a todo el trabajo que se realiza en c, sin embargo, es solo una suposición. Ahora los otros son más confusos/supresores. ¿Los enunciados try/except son tan caros como parecen en este contexto o está sucediendo algo más?

¡Cualquier ayuda para explicar estas diferencias sería muy apreciada! Disculpas por la larga publicación.

+2

Puede intentar usar tupla en lugar de ['x', 'y', 'z'] (cuando sea posible): la construcción de tuplas es un poco más rápida que la de la lista. –

Respuesta

2

Algunas ideas fuera de mi cabeza; lo siento si no son lo suficientemente palpable:

  • El primer iterador está utilizando pop de la lista para implementar next, es decir, la lista está mutado después de cada elemento se recupera. Quizás esta mutación de una estructura de datos compuesta asignada dinámicamente está reduciendo el rendimiento. Todo depende de los detalles de implementación de las listas, por lo que esto podría ser completamente irrelevante.
  • El último iterador está utilizando una característica conectada al lenguaje (rendimiento) en un contexto simple para producir el resultado. Supongo que esto indica que hay más espacio para la optimización que una clase de iterador personalizado que intenta obtener el mismo resultado.
  • El 5º temporizador no utiliza un iterador personalizado, sino que usa el proporcionado para las listas directamente. Los iteradores de listas probablemente estén bien optimizados, y no existe una capa de direccionamiento indirecto que utilizan esas clases personalizadas, algunas de las cuales usan internamente un iterador de lista de todas formas.
+1

Agregaría que subir y atrapar una excepción es relativamente costoso: http://paltman.com/2008/01/18/try-except-performance-in-python-a-simple-test/. – Noah

+0

Ah, por supuesto. Es curioso cómo me olvido de las cosas realmente obvias =) Nunca entendí por qué Python y Ruby piensan que las excepciones deberían usarse para cosas como esas ... – Louis

Cuestiones relacionadas