2012-01-20 14 views
5

Sto riscontrando differenze di prestazioni abbastanza sorprendenti quando si esegue l'iterazione su un piccolo contenitore con un iteratore personalizzato. Speravo che qualcuno potesse aiutarmi a capire da dove vengono queste differenze.performance iteratore personalizzata

Primo contesto; Sto scrivendo un numero di moduli di estensione Python usando boost :: python, uno dei quali contiene un binding a un tipo di vettore float 3d che implementa il getitem. Dal momento che ha ottenuto la possibilità di iterare su di esso, tuttavia sembra abbastanza lento, ma non è ovvio perché così ho deciso di giocare con alcuni semplici iteratori personalizzati in python per avere un'idea migliore di come funzionano le cose. Che è dove questi iteratori provenivano da ...

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] 

successivo mi sono imbattuto questi attraverso uno script con il modulo timeit (che assume il codice di cui sopra è in un modulo chiamato 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() 

Questo stampa il seguente

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 

Non sorprende che il listenerator python sia il più veloce, con un margine. Presumo che questo dipenda da alcune ottimizzazioni magiche all'interno di python. Il generatore è anche considerevolmente più veloce delle classi MyIterator, che di nuovo non ne sono molto sorpreso, e presumo sia dovuto a tutto il lavoro che si sta facendo in c, ma questo è solo un'ipotesi. Ora gli altri sono più confusi/supplichevoli. Le dichiarazioni try/except sono costose come sembrano in questo contesto o qualcos'altro?

Qualsiasi aiuto nello spiegare queste differenze sarebbe molto apprezzato! Ci scusiamo per il lungo post.

+2

Si può provare a usare tupla invece di ['x', 'y', 'z'] (dove possibile): la costruzione di tuple è leggermente più veloce di quella di lista. –

risposta

2

Alcune idee fuori dalla mia testa; scusate se non sono abbastanza palpabile:

  • Il primo iteratore sta usando del pop lista di attuare next, cioè la lista è mutato dopo ogni elemento viene recuperato. Forse questa mutazione di una struttura di dati compositi allocata dinamicamente sta riducendo le prestazioni. Tutto dipende dai dettagli di implementazione degli elenchi, quindi questo potrebbe essere completamente irrilevante.
  • L'ultimo iteratore utilizza una funzionalità cablata nella lingua (resa) in un contesto semplice per produrre il risultato. Ad una ipotesi, dirò questo indica che c'è più spazio per l'ottimizzazione di una classe di iteratori personalizzati che tenta di ottenere lo stesso risultato.
  • Il 5 ° timer non sta utilizzando un iteratore personalizzato e sta invece utilizzando quello fornito direttamente per gli elenchi. Gli iteratori per gli elenchi sono probabilmente ben ottimizzati e non esiste uno strato di riferimento indiretto usato da quelle classi personalizzate, alcuni dei quali utilizzano comunque internamente un iteratore di tali elenchi.
+1

Aggiungo che sollevare e catturare un'eccezione è relativamente costoso: http://paltman.com/2008/01/18/try-except-performance-in-python-a-simple-test/. – Noah

+0

Ah, certo. Strano come le cose davvero ovvie mi mancano =) Non ho mai veramente capito perché Python e Ruby pensano che le eccezioni dovrebbero essere usate per cose del genere ... – Louis