2015-06-07 18 views
8

Ho riscontrato uno strano problema di prestazioni durante l'aggiunta a un membro della classe str in python 2.7.3. So che accedere alle variabili locali è più veloce, tuttavia, nel problema sottostante, c'è una differenza di velocità di oltre 100 volte tra i due loop. Quello che accede a.accum_ si avvia velocemente ma rallenta, come se str iadd sia O (n^2) con la lunghezza di str.membro python prestazioni troppo lente

Qualcuno conosce il motivo?

# Fast (< 1sec): 
accum = str() 
for ii in range(1000000): 
    if (ii % 10000) == 0: 
     print 'fast cnt', ii 
    accum += 'zzzzz\n' 

# Much slower (> 5 mins): 
class Foo: 
    pass 
a = Foo() 
a.accum_ = str() 
for ii in range(1000000): 
    if (ii % 10000) == 0: 
     print 'slow cnt', ii 
    a.accum_ += 'zzzzz\n' 
+0

È sicuramente perché nel primo approccio è il primo approccio la stringa avrà un solo riferimento durante la sua durata e CPython può ottimizzare questo caso, quindi O (N) tempo. Nel secondo caso il conteggio dei riferimenti è sicuramente aumentato da 1 (ma esattamente dove?), Quindi il tempo quadratico. –

+0

@AshwiniChaudhary: il conteggio dei riferimenti è lo stesso in entrambi i casi, ma come indica la risposta, c'è un ottimizzazione specifica rispetto al primo caso, ma non rispetto al secondo, ed è per questo che il secondo caso richiede un tempo quadratico. – doublep

+0

@doublep Non sono sicuro di come stai cercando di contare i riferimenti, ma controlla il refcount prima di '+ =' e ** durante ** '+ ='. Dovrebbe aumentare di almeno 1. –

risposta

3

stringhe di Python sono immutabili e quindi non possono avere un metodo di __iadd__. Ciò a cui si assiste nel primo esempio è una micro ottimizzazione dell'interprete CPython. Nel primo esempio l'interprete ha notato che ha una variabile locale che ha un conteggio di riferimento di 1. Quindi, l'interprete può sfacciatamente sfuggire modificando la stringa sul posto. Anche se questo viola il contratto di str, in nessun momento durante l'esecuzione del programma sarà evidente che questo contratto è stato brevemente violato.

Nell'ultimo esempio questa micro ottimizzazione non viene implementata, motivo per cui è così lento. Sembra che l'ottimizzazione possa essere applicata, quindi non sono sicuro del motivo per cui non viene applicato.

Generalmente, tuttavia, se si crea una stringa, raggruppare le sottostringhe in un elenco e quindi utilizzare str.join per creare il prodotto finale.

+1

L'ottimizzazione rilevante è in 'string_concatenate()' nel file 'ceval.c'. Ovviamente, questo è rilevante per CPython. – doublep

5

Per il primo esempio è piuttosto chiaro che è caso di ottimizzazione di riferimento unico (ci sono due riferimenti realtà: uno dall'oggetto stesso e uno LOAD_FAST; unicode_concatenate cercherà di ridurlo a 1 prima di passare il controllo al PyUnicode_Append) fatto da CPython utilizzando questa funzione unicode_modifiable:

static int 
unicode_modifiable(PyObject *unicode) 
{ 
    assert(_PyUnicode_CHECK(unicode)); 
    if (Py_REFCNT(unicode) != 1) 
     return 0; 
    if (_PyUnicode_HASH(unicode) != -1) 
     return 0; 
    if (PyUnicode_CHECK_INTERNED(unicode)) 
     return 0; 
    if (!PyUnicode_CheckExact(unicode)) 
     return 0; 
#ifdef Py_DEBUG 
    /* singleton refcount is greater than 1 */ 
    assert(!unicode_is_singleton(unicode)); 
#endif 
    return 1; 
} 

Ma nel secondo caso come dati di istanza viene memorizzato in un pitone dict piuttosto che una variabile semplice, così le cose si po 'diverso.

a.accum_ += 'foo' 

richiede effettivamente precaricare il valore di a.accum_ e riporlo nello stack. Quindi, ora la stringa ha almeno tre riferimenti: uno dal dizionario di istanza, uno da DUP_TOP e uno da PyObject_GetAttr utilizzato da LOAD_ATTR. Quindi, Python non può ottimizzare questo caso poiché la modifica di uno di essi influirà anche su altri riferimenti.

>>> class A: 
    pass 
... 
>>> a = A() 
>>> def func(): 
    a.str = 'spam' 
    print a.str 
    return '_from_func' 
... 
>>> a.str = 'foo' 
>>> a.str += func() 
spam 

che ci si aspetta l'uscita di qui per essere 'spam_from_func', ma sta per essere diverso, perché il valore originale di a.str è stato conservato da Python prima func() è stato chiamato.

>>> a.str 
'foo_from_func' 

Codice Byte:

>>> import dis 
>>> def func_class(): 
     a = Foo() 
     a.accum = '' 
     a.accum += 'zzzzz\n' 
... 
>>> dis.dis(func_class) 
    2   0 LOAD_GLOBAL    0 (Foo) 
       3 CALL_FUNCTION   0 (0 positional, 0 keyword pair) 
       6 STORE_FAST    0 (a) 

    3   9 LOAD_CONST    1 ('') 
      12 LOAD_FAST    0 (a) 
      15 STORE_ATTR    1 (accum) 

    4   18 LOAD_FAST    0 (a) 
      21 DUP_TOP 
      22 LOAD_ATTR    1 (accum) 
      25 LOAD_CONST    2 ('zzzzz\n') 
      28 INPLACE_ADD 
      29 ROT_TWO 
      30 STORE_ATTR    1 (accum) 
      33 LOAD_CONST    0 (None) 
      36 RETURN_VALUE 

Si noti che questa ottimizzazione è stato fatto in around 2004 (CPython 2.4) per impedire agli utenti la lentezza di a += ba += b o , quindi è principalmente pensato per variabili semplici e funziona solo se l'istruzione successiva è STORE_FAST (variabile locale), STORE_DEREF (chiusure) e STORE_NAME. Non è una soluzione generale, the best way to do this in Python is to create a list and join its items using str.join.

CPython dettaglio di implementazione: Se s e t sono entrambe le stringhe, alcune implementazioni di Python, come CPython solito può eseguire un ottimizzazione sul posto per le assegnazioni della forma s = s + t o s += t. Quando applicabile a , questa ottimizzazione rende probabile un tempo di esecuzione quadratico molto inferiore a . Questa ottimizzazione è dipendente dalla versione e dall'implementazione . Per codice sensibile alle prestazioni, è preferibile utilizzare il metodo str.join() che assicura la costante concatenazione lineare delle prestazioni tra versioni e implementazioni.

Problemi correlati