Ci sono numexpr , numba e cython in giro, l'obiettivo di questa risposta è di prendere in considerazione queste possibilità.
Ma prima diciamo l'ovvio: non importa come si mappa una funzione Python su un array numpy, rimane una funzione Python, questo significa per ogni valutazione:
- L'elemento numpy-array deve essere convertito in un oggetto Python (ad es
Float
. a).
- tutti i calcoli vengono eseguiti con oggetti Python, il che significa avere l'overhead di interprete, invio dinamico e oggetti immutabili.
Pertanto, quale macchinario viene effettivamente utilizzato per eseguire il loop nell'array non gioca un ruolo importante a causa del sovraccarico sopra menzionato: rimane molto più lento rispetto all'utilizzo della funzionalità integrata di numpy.
Diamo un'occhiata al seguente esempio:
# numpy-functionality
def f(x):
return x+2*x*x+4*x*x*x
# python-function as ufunc
import numpy as np
vf=np.vectorize(f)
vf.__name__="vf"
np.vectorize
viene scelto come rappresentante della classe di approcci della funzione pure-python. Usando perfplot
(vedi il codice nell'appendice di questa risposta) otteniamo i seguenti tempi di esecuzione:
Possiamo vedere che l'approccio intorpidito è 10x-100x più veloce della versione pura di Python. La riduzione delle prestazioni per array di dimensioni maggiori è probabilmente dovuta al fatto che i dati non si adattano più alla cache.
Vale anche la pena ricordare che vectorize
utilizza anche molta memoria, quindi spesso l'utilizzo della memoria è il collo di bottiglia (vedi la relativa domanda SO ). Si noti inoltre che la documentazione di quel numpy np.vectorize
afferma che è "fornita principalmente per comodità, non per prestazioni".
Altri strumenti dovrebbero essere usati, quando si desiderano prestazioni, oltre a scrivere un'estensione C da zero, ci sono le seguenti possibilità:
Si sente spesso che le prestazioni intorpidite sono buone quanto si ottiene, perché è pura C sotto il cofano. Eppure c'è molto margine di miglioramento!
La versione numpy vettorizzata utilizza molta memoria aggiuntiva e accessi alla memoria. La libreria Numexp tenta di affiancare gli array numpy e quindi ottenere un migliore utilizzo della cache:
# less cache misses than numpy-functionality
import numexpr as ne
def ne_f(x):
return ne.evaluate("x+2*x*x+4*x*x*x")
Porta al seguente confronto:
Non posso spiegare tutto nella trama sopra: all'inizio possiamo vedere un overhead più grande per la libreria numexpr, ma poiché utilizza meglio la cache è circa 10 volte più veloce per array più grandi!
Un altro approccio è compilare jit la funzione e ottenere così un vero UFunc in puro C. Questo è l'approccio di numba:
# runtime generated C-function as ufunc
import numba as nb
@nb.vectorize(target="cpu")
def nb_vf(x):
return x+2*x*x+4*x*x*x
È 10 volte più veloce dell'approccio intorpidito originale:
Tuttavia, il compito è parallelamente imbarazzante, quindi potremmo anche usarlo prange
per calcolare il ciclo in parallelo:
@nb.njit(parallel=True)
def nb_par_jitf(x):
y=np.empty(x.shape)
for i in nb.prange(len(x)):
y[i]=x[i]+2*x[i]*x[i]+4*x[i]*x[i]*x[i]
return y
Come previsto, la funzione parallela è più lenta per input più piccoli, ma più veloce (quasi fattore 2) per dimensioni più grandi:
Mentre numba è specializzata nell'ottimizzazione delle operazioni con array numpy, Cython è uno strumento più generale. È più complicato estrarre le stesse prestazioni di numba - spesso dipende da compilatore locale (gcc / MSVC) vs llvm (numba):
%%cython -c=/openmp -a
import numpy as np
import cython
#single core:
@cython.boundscheck(False)
@cython.wraparound(False)
def cy_f(double[::1] x):
y_out=np.empty(len(x))
cdef Py_ssize_t i
cdef double[::1] y=y_out
for i in range(len(x)):
y[i] = x[i]+2*x[i]*x[i]+4*x[i]*x[i]*x[i]
return y_out
#parallel:
from cython.parallel import prange
@cython.boundscheck(False)
@cython.wraparound(False)
def cy_par_f(double[::1] x):
y_out=np.empty(len(x))
cdef double[::1] y=y_out
cdef Py_ssize_t i
cdef Py_ssize_t n = len(x)
for i in prange(n, nogil=True):
y[i] = x[i]+2*x[i]*x[i]+4*x[i]*x[i]*x[i]
return y_out
Cython comporta funzioni leggermente più lente:
Conclusione
Ovviamente, testare solo per una funzione non dimostra nulla. Si dovrebbe anche tenere presente che, per l'esempio di funzione scelto, la larghezza di banda della memoria era il collo di bottiglia per dimensioni superiori a 10 ^ 5 elementi - quindi abbiamo avuto le stesse prestazioni per numba, numexpr e cython in questa regione.
Alla fine, la risposta definitiva dipende dal tipo di funzione, hardware, distribuzione Python e altri fattori. Per esempio Anaconda-distribuzione utilizza VML di Intel per le funzioni di NumPy e quindi Sorpassa numba (a meno che non usa SVML, vedere questo SO-post ) facilmente per funzioni trascendenti piace exp
, sin
, cos
e simili - si veda ad esempio il seguente SO-post .
Tuttavia, da questa indagine e dalla mia esperienza finora, direi che numba sembra essere lo strumento più semplice con le migliori prestazioni purché non siano coinvolte funzioni trascendentali.
Tracciare i tempi di esecuzione con perfplot -package:
import perfplot
perfplot.show(
setup=lambda n: np.random.rand(n),
n_range=[2**k for k in range(0,24)],
kernels=[
f,
vf,
ne_f,
nb_vf, nb_par_jitf,
cy_f, cy_par_f,
],
logx=True,
logy=True,
xlabel='len(x)'
)