Perché gli array di Python sono lenti?


153

Mi aspettavo array.arraydi essere più veloce delle liste, dato che le matrici sembrano essere senza box.

Tuttavia, ottengo il seguente risultato:

In [1]: import array

In [2]: L = list(range(100000000))

In [3]: A = array.array('l', range(100000000))

In [4]: %timeit sum(L)
1 loop, best of 3: 667 ms per loop

In [5]: %timeit sum(A)
1 loop, best of 3: 1.41 s per loop

In [6]: %timeit sum(L)
1 loop, best of 3: 627 ms per loop

In [7]: %timeit sum(A)
1 loop, best of 3: 1.39 s per loop

Quale potrebbe essere la causa di tale differenza?


4
gli strumenti numpy possono sfruttare in modo efficiente il tuo array:% timeit np.sum (A): 100 loop, meglio di 3: 8,87 ms per loop
BM

6
Non ho mai incontrato una situazione in cui avrei dovuto usare il arraypacchetto. Se vuoi fare notevoli quantità di matematica, Numpy opera alla velocità della luce (cioè C), e di solito meglio di ingenui implementazioni di cose come sum()).
Nick T,

40
Elettori vicini: perché esattamente questo è basato sull'opinione? Il PO sembra porre una domanda tecnica specifica su un fenomeno misurabile e ripetibile.
Kevin,

5
@NickT Leggi Un aneddoto sull'ottimizzazione . Si scopre che arrayè piuttosto veloce nel convertire una stringa di numeri interi (che rappresentano byte ASCII) in un stroggetto. Lo stesso Guido ha escogitato questo dopo molte altre soluzioni ed è stato piuttosto sorpreso dalle prestazioni. Comunque questo è l'unico posto dove ricordo di averlo visto utile. numpyè molto meglio per gestire gli array ma è una dipendenza di terze parti.
Bakuriu,

Risposte:


220

La memoria è "unboxed", ma ogni volta che accedi ad un elemento Python deve "boxare" (incorporarlo in un normale oggetto Python) per farci qualcosa. Ad esempio, l' sum(A)iterazione sull'array e box ogni intero, uno alla volta, in un normale intoggetto Python . Questo costa tempo. Nel tuo sum(L), tutto il pugilato è stato fatto al momento della creazione dell'elenco.

Quindi, alla fine, un array è generalmente più lento, ma richiede sostanzialmente meno memoria.


Ecco il codice pertinente di una versione recente di Python 3, ma le stesse idee di base si applicano a tutte le implementazioni di CPython dalla prima pubblicazione di Python.

Ecco il codice per accedere a un elemento dell'elenco:

PyObject *
PyList_GetItem(PyObject *op, Py_ssize_t i)
{
    /* error checking omitted */
    return ((PyListObject *)op) -> ob_item[i];
}

C'è davvero poco: somelist[i]restituisce semplicemente l' ioggetto nella lista (e tutti gli oggetti Python in CPython sono puntatori a una struttura il cui segmento iniziale è conforme al layout di a struct PyObject).

Ed ecco l' __getitem__implementazione per un arraycodice di tipo l:

static PyObject *
l_getitem(arrayobject *ap, Py_ssize_t i)
{
    return PyLong_FromLong(((long *)ap->ob_item)[i]);
}

La memoria grezza viene trattata come un vettore di C longinteri nativi della piattaforma ; il i' C longè letto; e quindi PyLong_FromLong()viene chiamato per avvolgere ("box") il nativo C longin un longoggetto Python (che, in Python 3, che elimina la distinzione di Python 2 tra inte long, viene effettivamente mostrato come tipo int).

Questo boxing deve allocare nuova memoria per un intoggetto Python e spruzzare C longi bit nativi in esso. Nel contesto dell'esempio originale, la durata di questo oggetto è molto breve (il tempo sufficiente per sum()aggiungere il contenuto in un totale parziale), quindi è necessario più tempo per deallocare il nuovo intoggetto.

Questo è il punto da cui proviene la differenza di velocità, da cui proviene sempre e dall'implementazione di CPython.


87

Per aggiungere all'eccellente risposta di Tim Peters, gli array implementano il protocollo buffer , mentre gli elenchi no. Ciò significa che, se stai scrivendo un'estensione C (o l'equivalente morale, come scrivere un modulo Cython ), puoi accedere e lavorare con gli elementi di un array molto più velocemente di qualsiasi cosa Python possa fare. Questo ti darà notevoli miglioramenti di velocità, forse ben oltre un ordine di grandezza. Tuttavia, ha una serie di aspetti negativi:

  1. Ora sei nel business di scrivere C invece di Python. Cython è un modo per migliorare questo, ma non elimina molte differenze fondamentali tra le lingue; devi conoscere la semantica C e capire cosa sta facendo.
  2. L'API C di PyPy funziona in una certa misura , ma non è molto veloce. Se stai prendendo di mira PyPy, probabilmente dovresti semplicemente scrivere un codice semplice con elenchi regolari e quindi lasciare che JITter lo ottimizzi per te.
  3. Le estensioni C sono più difficili da distribuire rispetto al puro codice Python perché devono essere compilate. La compilazione tende a dipendere dall'architettura e dal sistema operativo, quindi dovrai assicurarti di compilare per la tua piattaforma di destinazione.

Passare direttamente alle estensioni C potrebbe essere usare una mazza per schiacciare una mosca, a seconda del caso d'uso. Dovresti prima investigare su NumPy e vedere se è abbastanza potente da fare qualunque cosa tu stia facendo. Sarà anche molto più veloce di Python nativo, se usato correttamente.


10

Tim Peters ha risposto perché questo è lento, ma vediamo come migliorarlo .

Attenendosi al tuo esempio di sum(range(...))(fattore 10 più piccolo del tuo esempio per adattarlo alla memoria qui):

import numpy
import array
L = list(range(10**7))
A = array.array('l', L)
N = numpy.array(L)

%timeit sum(L)
10 loops, best of 3: 101 ms per loop

%timeit sum(A)
1 loop, best of 3: 237 ms per loop

%timeit sum(N)
1 loop, best of 3: 743 ms per loop

In questo modo anche numpy deve box / unbox, che ha un sovraccarico aggiuntivo. Per renderlo veloce bisogna rimanere nel codice c intorpidito:

%timeit N.sum()
100 loops, best of 3: 6.27 ms per loop

Quindi dalla soluzione elenco alla versione numpy questo è un fattore 16 in fase di esecuzione.

Controlliamo anche quanto tempo richiede la creazione di tali strutture dati

%timeit list(range(10**7))
1 loop, best of 3: 283 ms per loop

%timeit array.array('l', range(10**7))
1 loop, best of 3: 884 ms per loop

%timeit numpy.array(range(10**7))
1 loop, best of 3: 1.49 s per loop

%timeit numpy.arange(10**7)
10 loops, best of 3: 21.7 ms per loop

Chiaro vincitore: Numpy

Si noti inoltre che la creazione della struttura dei dati richiede circa il tempo necessario per sommare, se non di più. L'allocazione della memoria è lenta.

Utilizzo della memoria di quelli:

sys.getsizeof(L)
90000112
sys.getsizeof(A)
81940352
sys.getsizeof(N)
80000096

Quindi questi richiedono 8 byte per numero con un sovraccarico variabile. Per la gamma che utilizziamo sono sufficienti 32 bit, quindi possiamo proteggere un po 'di memoria.

N=numpy.arange(10**7, dtype=numpy.int32)

sys.getsizeof(N)
40000096

%timeit N.sum()
100 loops, best of 3: 8.35 ms per loop

Ma risulta che aggiungere 64 bit è più veloce di 32 bit sulla mia macchina, quindi ne vale la pena solo se si è limitati dalla memoria / larghezza di banda.


-1

si prega di notare che 100000000equivale a 10^8non farlo 10^7e i miei risultati sono i seguenti:

100000000 == 10**8

# my test results on a Linux virtual machine:
#<L = list(range(100000000))> Time: 0:00:03.263585
#<A = array.array('l', range(100000000))> Time: 0:00:16.728709
#<L = list(range(10**8))> Time: 0:00:03.119379
#<A = array.array('l', range(10**8))> Time: 0:00:18.042187
#<A = array.array('l', L)> Time: 0:00:07.524478
#<sum(L)> Time: 0:00:01.640671
#<np.sum(L)> Time: 0:00:20.762153
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.