Lorsque le tableau 2d (ou nd-tableau) est contigu en C ou F, alors cette tâche de mappage d'une fonction sur un tableau 2d est pratiquement la même que la tâche de mappage d'une fonction sur un tableau 1d - nous venons de doivent le voir de cette façon, par exemple via np.ravel(A,'K')
.
Une solution possible pour 1d-array a été discutée par exemple ici .
Cependant, lorsque la mémoire du 2d-array n'est pas contiguë, alors la situation est un peu plus compliquée, car on voudrait éviter d'éventuels échecs de cache si les axes sont traités dans le mauvais ordre.
Numpy dispose déjà d'une machine pour traiter les axes dans le meilleur ordre possible. Une possibilité d'utiliser cette machine est np.vectorize
. Cependant, la documentation de numpy np.vectorize
indique qu'elle est "fournie principalement pour la commodité, pas pour les performances" - une fonction python lente reste une fonction python lente avec toute la surcharge associée! Un autre problème est son énorme consommation de mémoire - voir par exemple ce message SO .
Quand on veut avoir une performance d'une fonction C mais utiliser la machinerie de numpy, une bonne solution est d'utiliser numba pour la création d'ufuncs, par exemple:
# 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
Il bat facilement np.vectorize
mais aussi quand la même fonction serait exécutée que la multiplication / addition de numpy-array, ie
# 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"
Voir l'annexe de cette réponse pour le code de mesure du temps:
La version de Numba (verte) est environ 100 fois plus rapide que la fonction python (ie np.vectorize
), ce qui n'est pas surprenant. Mais c'est également environ 10 fois plus rapide que la fonctionnalité numpy, car la version numbas n'a pas besoin de tableaux intermédiaires et utilise donc le cache plus efficacement.
Bien que l'approche ufunc de numba soit un bon compromis entre convivialité et performances, ce n'est toujours pas le mieux que nous puissions faire. Pourtant, il n’existe pas de solution miracle ou d’approche idéale pour une tâche quelconque - il faut comprendre quelles sont les limites et comment elles peuvent être atténuées.
Par exemple, pour les fonctions transcendantes (par exemple exp
, sin
, cos
) numba ne fournit pas d'avantages par rapport de numpy np.exp
(il n'y a pas de tableaux temporaires créés - la principale source de la vitesse-up). Cependant, mon installation Anaconda utilise le VML d'Intel pour les vecteurs supérieurs à 8192 - il ne peut tout simplement pas le faire si la mémoire n'est pas contiguë. Il serait donc préférable de copier les éléments dans une mémoire contiguë afin de pouvoir utiliser le VML d'Intel:
import numba as nb
@nb.vectorize(target="cpu")
def nb_vexp(x):
return np.exp(x)
def np_copy_exp(x):
copy = np.ravel(x, 'K')
return np.exp(copy).reshape(x.shape)
Pour l'équité de la comparaison, j'ai désactivé la parallélisation de VML (voir code en annexe):
Comme on peut le voir, une fois que VML entre en jeu, la surcharge de copie est plus que compensée. Pourtant, une fois que les données deviennent trop volumineuses pour le cache L3, l'avantage est minime car la tâche redevient liée à la bande passante mémoire.
D'un autre côté, numba pourrait également utiliser le SVML d'Intel, comme expliqué dans cet article :
from llvmlite import binding
# set before import
binding.set_option('SVML', '-vector-library=SVML')
import numba as nb
@nb.vectorize(target="cpu")
def nb_vexp_svml(x):
return np.exp(x)
et l'utilisation de VML avec des rendements de parallélisation:
La version de numba a moins de frais généraux, mais pour certaines tailles, VML bat SVML même malgré la surcharge de copie supplémentaire - ce qui n'est pas un peu surprenant car les ufuncs de numba ne sont pas parallélisés.
Annonces:
A. comparaison de la fonction polynomiale:
import perfplot
perfplot.show(
setup=lambda n: np.random.rand(n,n)[::2,::2],
n_range=[2**k for k in range(0,12)],
kernels=[
f,
vf,
nb_vf
],
logx=True,
logy=True,
xlabel='len(x)'
)
B. comparaison de exp
:
import perfplot
import numexpr as ne # using ne is the easiest way to set vml_num_threads
ne.set_vml_num_threads(1)
perfplot.show(
setup=lambda n: np.random.rand(n,n)[::2,::2],
n_range=[2**k for k in range(0,12)],
kernels=[
nb_vexp,
np.exp,
np_copy_exp,
],
logx=True,
logy=True,
xlabel='len(x)',
)