2014-04-13 10 views
7

Una volta, dopo aver osservato il tutorial sull'ottimizzazione delle prestazioni di Mike Muller (penso this one), un pensiero ha iniziato a vivere nella mia testa: se le prestazioni sono importanti, minimizza l'accesso agli elementi nel ciclo per indice, e. g. se è necessario accedere a x[1] più volte in un loop for x in l - assegnare una variabile a x[1] e riutilizzarla nel ciclo.Per decomprimere elementi ciclo

ora ho questo esempio di sintesi:

import timeit 

SEQUENCE = zip(range(1000), range(1, 1001)) 

def no_unpacking(): 
    return [item[0] + item[1] for item in SEQUENCE] 


def unpacking():  
    return [a + b for a, b in SEQUENCE] 


print timeit.Timer('no_unpacking()', 'from __main__ import no_unpacking').timeit(10000) 
print timeit.Timer('unpacking()', 'from __main__ import unpacking').timeit(10000) 

unpacking() e no_unpacking() funzioni restituiscono lo stesso risultato. L'implementazione è diversa: unpacking() decomprime gli articoli in a e b nel ciclo; no_unpacking() ottiene valori per indici.

Per python27, mostra:

1.25280499458 
0.946601867676 

in altre parole, unpacking() sorpassa no_unpacking() da ~ 25%.

La domanda è:

  • Perché accesso da Index cose rallentare in modo significativo (anche in questo caso semplice)?

Domanda bonus:

  • Ho anche provato questo su pypy - non v'è quasi alcuna differenza tra queste 2 funzioni, prestazioni-saggio. Perché?

Grazie per qualsiasi aiuto.

risposta

22

Per rispondere alle vostre domande possiamo controllare il bytecode generato dalle due funzioni utilizzando il modulo dis:

In [5]: def no_unpacking(): 
    ...:  s = [] 
    ...:  for item in SEQUENCE: 
    ...:   s.append(item[0] + item[1]) 
    ...:  return s 
    ...: 
    ...: 
    ...: def unpacking(): 
    ...:  s = [] 
    ...:  for a,b in SEQUENCE: 
    ...:   s.append(a+b) 
    ...:  return s 

ho ampliato l'elenco-di comprensione perché in python3 sarebbe più ingombrante per controllare l'interessante bytecode. Il codice è equivalente quindi non ha importanza per il nostro scopo.

Il codice byte per la prima funzione è:

In [6]: dis.dis(no_unpacking) 
    2   0 BUILD_LIST    0 
       3 STORE_FAST    0 (s) 

    3   6 SETUP_LOOP    39 (to 48) 
       9 LOAD_GLOBAL    0 (SEQUENCE) 
      12 GET_ITER    
     >> 13 FOR_ITER    31 (to 47) 
      16 STORE_FAST    1 (item) 

    4   19 LOAD_FAST    0 (s) 
      22 LOAD_ATTR    1 (append) 
      25 LOAD_FAST    1 (item) 
      28 LOAD_CONST    1 (0) 
      31 BINARY_SUBSCR   
      32 LOAD_FAST    1 (item) 
      35 LOAD_CONST    2 (1) 
      38 BINARY_SUBSCR   
      39 BINARY_ADD   
      40 CALL_FUNCTION   1 (1 positional, 0 keyword pair) 
      43 POP_TOP    
      44 JUMP_ABSOLUTE   13 
     >> 47 POP_BLOCK    

    5  >> 48 LOAD_FAST    0 (s) 
      51 RETURN_VALUE  

nota che il ciclo deve chiamare BINARY_SUBSCR due volte per i due elementi della tupla.

Il bytecode per la seconda funzione è:

In [7]: dis.dis(unpacking) 
    9   0 BUILD_LIST    0 
       3 STORE_FAST    0 (s) 

10   6 SETUP_LOOP    37 (to 46) 
       9 LOAD_GLOBAL    0 (SEQUENCE) 
      12 GET_ITER    
     >> 13 FOR_ITER    29 (to 45) 
      16 UNPACK_SEQUENCE   2 
      19 STORE_FAST    1 (a) 
      22 STORE_FAST    2 (b) 

11   25 LOAD_FAST    0 (s) 
      28 LOAD_ATTR    1 (append) 
      31 LOAD_FAST    1 (a) 
      34 LOAD_FAST    2 (b) 
      37 BINARY_ADD   
      38 CALL_FUNCTION   1 (1 positional, 0 keyword pair) 
      41 POP_TOP    
      42 JUMP_ABSOLUTE   13 
     >> 45 POP_BLOCK    

12  >> 46 LOAD_FAST    0 (s) 
      49 RETURN_VALUE 

Si noti come non v'è alcun BINARY_SUBSCR da eseguire.

Così, sembra che UNPACK_SEQUENCE più uno STORE_FAST (che è le operazioni extra aggiunti dal disimballo) sono più veloce poi fare due BINARY_SUBSCR. Questo è ragionevole poiché BINARY_SUBSCR è una chiamata di metodo completa, mentre UNPACK_SEQUENCE e STORE_FAST sono operazioni più semplici.

si può vedere la differenza anche nei casi più semplici:

In [1]: def iter_with_index(s): 
    ...:  for i in range(len(s)): 
    ...:   s[i] 
    ...:   

In [2]: def iter_without_index(s): 
    ...:  for el in s:el 
    ...:  

In [3]: %%timeit s = 'a' * 10000 
    ...: iter_with_index(s) 
    ...: 
1000 loops, best of 3: 583 us per loop 

In [4]: %%timeit s = 'a' * 10000 
    ...: iter_without_index(s) 
    ...: 
1000 loops, best of 3: 206 us per loop 

Come si può vedere l'iterazione di una stringa è circa 3 volte più lento utilizzando l'indicizzazione esplicita. Questo è tutto il sovraccarico dovuto alle chiamate a BINARY_SUBSCR.

Riguardo alla seconda domanda: Pypy ha JIT che è in grado di analizzare il codice e produce una versione ottimizzata che evita il sovraccarico delle operazioni di indicizzazione. Quando si rende conto che l'abbonamento viene eseguito su tuple, probabilmente è in grado di produrre codice che non chiama il metodo tuple ma accede direttamente agli elementi, rimuovendo così completamente le operazioni BINARY_SUBSCR.

+0

Grande spiegazione, buono a sapersi che non ero interessato senza motivo. – alecxe

+0

Ottima risposta. Molto accurato – fr1tz

+2

Davvero una bella spiegazione. Comunque, cambiando 'range' in' xrange', la velocità del mio notebook sta cambiando da 4.0 us giù a 3.43 us –

Problemi correlati