Comment optimisez-vous ce code ( sans vectorisation, car cela conduit à utiliser la sémantique du calcul, ce qui est assez souvent loin d'être non trivial):
slow_lib.py:
import numpy as np
def foo():
size = 200
np.random.seed(1000031212)
bar = np.random.Rand(size, size)
moo = np.zeros((size,size), dtype = np.float)
for i in range(0,size):
for j in range(0,size):
val = bar[j]
moo += np.outer(val, val)
Le fait est que de telles boucles de type correspondent assez souvent à des opérations où vous avez des doubles sommes sur une opération vectorielle.
C'est assez lent:
>>t = timeit.timeit('foo()', 'from slow_lib import foo', number = 10)
>>print ("took: "+str(t))
took: 41.165681839
Ok, alors cynothize et ajoutons des annotations de type comme il n'y a pas de lendemain:
c_slow_lib.pyx:
import numpy as np
cimport numpy as np
import cython
@cython.boundscheck(False)
@cython.wraparound(False)
def foo():
cdef int size = 200
cdef int i,j
np.random.seed(1000031212)
cdef np.ndarray[np.double_t, ndim=2] bar = np.random.Rand(size, size)
cdef np.ndarray[np.double_t, ndim=2] moo = np.zeros((size,size), dtype = np.float)
cdef np.ndarray[np.double_t, ndim=1] val
for i in xrange(0,size):
for j in xrange(0,size):
val = bar[j]
moo += np.outer(val, val)
>>t = timeit.timeit('foo()', 'from c_slow_lib import foo', number = 10)
>>print ("took: "+str(t))
took: 42.3104710579
... ehr ... quoi? Numba à la rescousse!
numba_slow_lib.py:
import numpy as np
from numba import jit
size = 200
np.random.seed(1000031212)
bar = np.random.Rand(size, size)
@jit
def foo():
bar = np.random.Rand(size, size)
moo = np.zeros((size,size), dtype = np.float)
for i in range(0,size):
for j in range(0,size):
val = bar[j]
moo += np.outer(val, val)
>>t = timeit.timeit('foo()', 'from numba_slow_lib import foo', number = 10)
>>print("took: "+str(t))
took: 40.7327859402
Alors, n'y a-t-il vraiment aucun moyen d'accélérer cela? Le point est:
Voici le code pour outer
:
def outer(a, b, out=None):
a = asarray(a)
b = asarray(b)
return multiply(a.ravel()[:, newaxis], b.ravel()[newaxis,:], out)
Ainsi, chaque appel à outer
implique un certain nombre d'appels python. Ceux-ci finissent par appeler du code compilé pour effectuer la multiplication. Mais chacun entraîne une surcharge qui n'a rien à voir avec la taille de vos tableaux.
Ainsi, 200 (200 ** 2?) Appels à outer
auront tout ce surdébit, tandis qu'un appel à outer
avec les 200 lignes a un surdébit défini, suivi d'une opération de compilation rapide.
cython
et numba
ne compilent pas ou ne contournent pas le code Python dans outer
. Tout ce qu'ils peuvent faire est de rationaliser le code d'itération qui vous avez écrit - et cela ne prend pas beaucoup de temps.
Sans entrer dans les détails, le jit MATLAB doit pouvoir remplacer le 'externe' par du code plus rapide - il réécrit l'itération. Mais mon expérience avec MATLAB remonte à une époque antérieure à sa vie.
Pour des améliorations réelles de la vitesse avec cython
et numba
, vous devez utiliser du code numpy/python primitif jusqu'en bas. Ou mieux encore concentrez vos efforts sur les pièces intérieures lentes.
Le remplacement de votre outer
par une version simplifiée réduit de moitié le temps d'exécution:
def foo1(N):
size = N
np.random.seed(1000031212)
bar = np.random.Rand(size, size)
moo = np.zeros((size,size), dtype = np.float)
for i in range(0,size):
for j in range(0,size):
val = bar[j]
moo += val[:,None]*val
return moo
Avec le N=200
Complet, votre fonction prenait 17 secondes par boucle. Si je remplace les deux lignes intérieures par pass
(pas de calcul), le temps passe à 3 ms par boucle. En d'autres termes, le mécanisme de boucle externe n'est pas un gros consommateur de temps, du moins pas comparé à de nombreux appels à outer()
.
Si la mémoire le permet, vous pouvez utiliser np.einsum
pour effectuer ces calculs lourds de manière vectorisée, comme ceci -
moo = size*np.einsum('ij,ik->jk',bar,bar)
On peut également utiliser np.tensordot
-
moo = size*np.tensordot(bar,bar,axes=(0,0))
Ou simplement np.dot
-
moo = size*bar.T.dot(bar)
De nombreux tutoriels et démonstrations de Cython, Numba, etc. donnent l'impression que ces outils peuvent accélérer votre code de manière automatique, mais en pratique, ce n'est souvent pas le cas: vous devrez modifier un peu votre code pour en extraire le meilleur. performance. Si vous avez déjà implémenté un certain degré de vectorisation, cela signifie généralement écrire TOUTES les boucles. Les raisons pour lesquelles les opérations du tableau Numpy ne sont pas optimales sont les suivantes:
Utiliser Numba ou Cython n'optimise pas ces problèmes! Au lieu de cela, ces outils vous permettent d'écrire du code en boucle qui est beaucoup plus rapide que le Python ordinaire.
De plus, pour Numba en particulier, vous devez être conscient de la différence entre "mode objet" et "mode nopython" . Les boucles serrées de votre exemple doivent s'exécuter en mode nopython pour fournir une accélération significative. Cependant, numpy.outer
Est pas encore pris en charge par Numba , ce qui entraîne la compilation de la fonction en mode objet. Décorez avec jit(nopython=True)
pour permettre à de tels cas de lever une exception.
Un exemple pour démontrer une accélération est en effet possible:
import numpy as np
from numba import jit
@jit
def foo_nb(bar):
size = bar.shape[0]
moo = np.zeros((size, size))
for i in range(0,size):
for j in range(0,size):
val = bar[j]
moo += np.outer(val, val)
return moo
@jit
def foo_nb2(bar):
size = bar.shape[0]
moo = np.zeros((size, size))
for i in range(size):
for j in range(size):
for k in range(0,size):
for l in range(0,size):
moo[k,l] += bar[j,k] * bar[j,l]
return moo
size = 100
bar = np.random.Rand(size, size)
np.allclose(foo_nb(bar), foo_nb2(bar))
# True
%timeit foo_nb(bar)
# 1 loop, best of 3: 816 ms per loop
%timeit foo_nb2(bar)
# 10 loops, best of 3: 176 ms per loop