Il y a numexpr , numba et cython autour, le but de cette réponse est de prendre ces possibilités en considération.
Mais disons d'abord l'évidence: peu importe comment vous mappez une fonction Python sur un tableau numpy, elle reste une fonction Python, ce qui signifie pour chaque évaluation:
- L'élément numpy-array doit être converti en objet Python (par exemple a
Float
).
- tous les calculs sont effectués avec des objets Python, ce qui signifie avoir la surcharge d'interprète, de répartition dynamique et d'objets immuables.
La machine utilisée pour boucler le tableau ne joue donc pas un grand rôle en raison de la surcharge mentionnée ci-dessus - elle reste beaucoup plus lente que l'utilisation de la fonctionnalité intégrée de numpy.
Jetons un œil à l'exemple suivant:
# 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
est choisi comme représentant de la classe d'approches de fonction en python pur. En utilisant perfplot
(voir le code en annexe de cette réponse) nous obtenons les durées de fonctionnement suivantes:
Nous pouvons voir que l'approche numpy est 10x-100x plus rapide que la version pure python. La diminution des performances pour les tailles de baie plus importantes est probablement due au fait que les données ne correspondent plus au cache.
Il convient également de mentionner, qui vectorize
utilise également beaucoup de mémoire, l'utilisation de la mémoire est donc souvent le goulot d'étranglement (voir la question SO connexe ). Notez également que la documentation de numpy sur np.vectorize
indique qu'elle est "fournie principalement pour des raisons de commodité et non pour des performances".
D'autres outils doivent être utilisés, lorsque des performances sont souhaitées, outre l'écriture d'une extension C à partir de zéro, il existe les possibilités suivantes:
On entend souvent que la performance numpy est aussi bonne que possible, car elle est en pur C sous le capot. Pourtant, il y a encore beaucoup à faire!
La version numpy vectorisée utilise beaucoup de mémoire supplémentaire et d'accès à la mémoire. Numexp-library essaie de paver les tableaux numpy et ainsi obtenir une meilleure utilisation du 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")
Conduit à la comparaison suivante:
Je ne peux pas tout expliquer dans l'intrigue ci-dessus: nous pouvons voir des frais généraux plus importants pour numexpr-library au début, mais parce qu'il utilise mieux le cache, il est environ 10 fois plus rapide pour les tableaux plus gros!
Une autre approche consiste à compiler jit la fonction et à obtenir ainsi un véritable UFunc pur-C. Voici l'approche de 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
C'est 10 fois plus rapide que l'approche numpy originale:
Cependant, la tâche est embarrassablement parallélisable, nous pourrions donc également l'utiliser prange
pour calculer la boucle en parallèle:
@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
Comme prévu, la fonction parallèle est plus lente pour les petites entrées, mais plus rapide (presque facteur 2) pour les grandes tailles:
Alors que numba est spécialisé dans l'optimisation des opérations avec les tableaux numpy, Cython est un outil plus général. Il est plus compliqué d'extraire les mêmes performances qu'avec numba - il s'agit souvent de llvm (numba) vs compilateur local (gcc / MSVC):
%%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 entraîne des fonctions un peu plus lentes:
Conclusion
De toute évidence, le test d'une seule fonction ne prouve rien. Il convient également de garder à l'esprit que pour l'exemple de fonction choisi, la bande passante de la mémoire était le col de la bouteille pour les tailles supérieures à 10 ^ 5 éléments - nous avons donc eu les mêmes performances pour numba, numexpr et cython dans cette région.
En fin de compte, la réponse ultime dépend du type de fonction, du matériel, de la distribution Python et d'autres facteurs. Par exemple Anaconda distribution utilise VML d'Intel pour les fonctions de numpy et donc surclasse Numba ( à moins qu'il utilise SVML, voir ce SO-post ) pour les fonctions transcendantes facilement aiment exp
, sin
, cos
et même - voir par exemple les éléments suivants SO-post .
Pourtant, à partir de cette enquête et de mon expérience jusqu'à présent, je dirais que le numba semble être l'outil le plus simple avec les meilleures performances tant qu'aucune fonction transcendantale n'est impliquée.
Tracer les temps de fonctionnement avec 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)'
)