Commençons par trois tableaux de dtype=np.double
. Les temporisations sont effectuées sur un processeur Intel à l'aide de numpy 1.7.1 compilé avec icc
et lié au mkl
d'Intel. Un processeur AMD avec numpy 1.6.1 compilé avec gcc
sans mkl
a également été utilisé pour vérifier les timings. Veuillez noter que les timings évoluent de manière presque linéaire avec la taille du système et ne sont pas dus au petit surcoût encouru dans les fonctions numpy if
, cette différence apparaîtra en microsecondes et non en millisecondes:
arr_1D=np.arange(500,dtype=np.double)
large_arr_1D=np.arange(100000,dtype=np.double)
arr_2D=np.arange(500**2,dtype=np.double).reshape(500,500)
arr_3D=np.arange(500**3,dtype=np.double).reshape(500,500,500)
Voyons d'abord le np.sum
fonction:
np.all(np.sum(arr_3D)==np.einsum('ijk->',arr_3D))
True
%timeit np.sum(arr_3D)
10 loops, best of 3: 142 ms per loop
%timeit np.einsum('ijk->', arr_3D)
10 loops, best of 3: 70.2 ms per loop
Pouvoirs:
np.allclose(arr_3D*arr_3D*arr_3D,np.einsum('ijk,ijk,ijk->ijk',arr_3D,arr_3D,arr_3D))
True
%timeit arr_3D*arr_3D*arr_3D
1 loops, best of 3: 1.32 s per loop
%timeit np.einsum('ijk,ijk,ijk->ijk', arr_3D, arr_3D, arr_3D)
1 loops, best of 3: 694 ms per loop
Produit extérieur:
np.all(np.outer(arr_1D,arr_1D)==np.einsum('i,k->ik',arr_1D,arr_1D))
True
%timeit np.outer(arr_1D, arr_1D)
1000 loops, best of 3: 411 us per loop
%timeit np.einsum('i,k->ik', arr_1D, arr_1D)
1000 loops, best of 3: 245 us per loop
Tous ces éléments sont deux fois plus rapides avec np.einsum
. Il devrait s'agir de comparaisons entre pommes et pommes, car tout est spécifiquement de dtype=np.double
. Je m'attendrais à une accélération dans une opération comme celle-ci:
np.allclose(np.sum(arr_2D*arr_3D),np.einsum('ij,oij->',arr_2D,arr_3D))
True
%timeit np.sum(arr_2D*arr_3D)
1 loops, best of 3: 813 ms per loop
%timeit np.einsum('ij,oij->', arr_2D, arr_3D)
10 loops, best of 3: 85.1 ms per loop
Einsum semble être au moins deux fois plus rapide pour np.inner
, np.outer
, np.kron
, et np.sum
quelle que soit la sélection de axes
. La principale exception étant np.dot
car il appelle DGEMM à partir d'une bibliothèque BLAS. Alors pourquoi np.einsum
plus rapide que d'autres fonctions numpy équivalentes?
Le cas de l'exhaustivité de la DGEMM:
np.allclose(np.dot(arr_2D,arr_2D),np.einsum('ij,jk',arr_2D,arr_2D))
True
%timeit np.einsum('ij,jk',arr_2D,arr_2D)
10 loops, best of 3: 56.1 ms per loop
%timeit np.dot(arr_2D,arr_2D)
100 loops, best of 3: 5.17 ms per loop
La théorie principale est du commentaire de @sebergs que np.einsum
peut utiliser SSE2 , mais les ufuncs de numpy ne le seront que jusqu'à numpy 1.8 (voir journal des modifications ). Je pense que c'est la bonne réponse, mais pas pu le confirmer. Une preuve limitée peut être trouvée en changeant le type de tableau d'entrée et en observant la différence de vitesse et le fait que tout le monde n'observe pas les mêmes tendances dans les synchronisations.
Maintenant que numpy 1.8 est sorti, où, selon la documentation, tous les ufunc devraient utiliser SSE2, je voulais vérifier que le commentaire de Seberg sur SSE2 était valide.
Pour effectuer le test, une nouvelle installation python 2.7 a été créée - numpy 1.7 et 1.8 ont été compilées avec icc
en utilisant des options standard sur un noyau opteron AMD exécutant Ubuntu.
Il s'agit du test effectué avant et après la mise à niveau 1.8:
import numpy as np
import timeit
arr_1D=np.arange(5000,dtype=np.double)
arr_2D=np.arange(500**2,dtype=np.double).reshape(500,500)
arr_3D=np.arange(500**3,dtype=np.double).reshape(500,500,500)
print 'Summation test:'
print timeit.timeit('np.sum(arr_3D)',
'import numpy as np; from __main__ import arr_1D, arr_2D, arr_3D',
number=5)/5
print timeit.timeit('np.einsum("ijk->", arr_3D)',
'import numpy as np; from __main__ import arr_1D, arr_2D, arr_3D',
number=5)/5
print '----------------------\n'
print 'Power test:'
print timeit.timeit('arr_3D*arr_3D*arr_3D',
'import numpy as np; from __main__ import arr_1D, arr_2D, arr_3D',
number=5)/5
print timeit.timeit('np.einsum("ijk,ijk,ijk->ijk", arr_3D, arr_3D, arr_3D)',
'import numpy as np; from __main__ import arr_1D, arr_2D, arr_3D',
number=5)/5
print '----------------------\n'
print 'Outer test:'
print timeit.timeit('np.outer(arr_1D, arr_1D)',
'import numpy as np; from __main__ import arr_1D, arr_2D, arr_3D',
number=5)/5
print timeit.timeit('np.einsum("i,k->ik", arr_1D, arr_1D)',
'import numpy as np; from __main__ import arr_1D, arr_2D, arr_3D',
number=5)/5
print '----------------------\n'
print 'Einsum test:'
print timeit.timeit('np.sum(arr_2D*arr_3D)',
'import numpy as np; from __main__ import arr_1D, arr_2D, arr_3D',
number=5)/5
print timeit.timeit('np.einsum("ij,oij->", arr_2D, arr_3D)',
'import numpy as np; from __main__ import arr_1D, arr_2D, arr_3D',
number=5)/5
print '----------------------\n'
Numpy 1.7.1:
Summation test:
0.172988510132
0.0934836149216
----------------------
Power test:
1.93524689674
0.839519000053
----------------------
Outer test:
0.130380821228
0.121401786804
----------------------
Einsum test:
0.979052495956
0.126066613197
Numpy 1.8:
Summation test:
0.116551589966
0.0920487880707
----------------------
Power test:
1.23683619499
0.815982818604
----------------------
Outer test:
0.131808176041
0.127472200394
----------------------
Einsum test:
0.781750011444
0.129271841049
Je pense que cela est assez concluant que SSE joue un grand rôle dans les différences de synchronisation, il convient de noter que la répétition de ces tests les synchronisations très par seulement ~ 0,003 s. La différence restante doit être couverte dans les autres réponses à cette question.
Tout d'abord, il y a eu beaucoup de discussions passées à ce sujet sur la liste numpy. Par exemple, voir: http://numpy-discussion.10968.n7.nabble.com/poor-performance-of-sum-with-sub-machine-Word-integer-types-td41.html = http://numpy-discussion.10968.n7.nabble.com/odd-performance-of-sum-td3332.html
Certains se résument au fait que einsum
est nouveau, et essaie probablement d'être meilleur sur l'alignement du cache et d'autres problèmes d'accès à la mémoire, tandis que la plupart des anciennes fonctions numpy se concentrent sur une implémentation facilement portable sur une interface fortement optimisée une. Je ne fais que spéculer, cependant.
Cependant, une partie de ce que vous faites n'est pas tout à fait une comparaison "pommes à pommes".
En plus de ce que @Jamie a déjà dit, sum
utilise un accumulateur plus approprié pour les tableaux
Par exemple, sum
fait plus attention à vérifier le type de l'entrée et à utiliser un accumulateur approprié. Par exemple, tenez compte des éléments suivants:
In [1]: x = 255 * np.ones(100, dtype=np.uint8)
In [2]: x
Out[2]:
array([255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255], dtype=uint8)
Notez que sum
est correct:
In [3]: x.sum()
Out[3]: 25500
Alors que einsum
donnera le mauvais résultat:
In [4]: np.einsum('i->', x)
Out[4]: 156
Mais si nous utilisons un dtype
moins limité, nous obtiendrons toujours le résultat que vous attendez:
In [5]: y = 255 * np.ones(100)
In [6]: np.einsum('i->', y)
Out[6]: 25500.0
Je pense que ces horaires expliquent ce qui se passe:
a = np.arange(1000, dtype=np.double)
%timeit np.einsum('i->', a)
100000 loops, best of 3: 3.32 us per loop
%timeit np.sum(a)
100000 loops, best of 3: 6.84 us per loop
a = np.arange(10000, dtype=np.double)
%timeit np.einsum('i->', a)
100000 loops, best of 3: 12.6 us per loop
%timeit np.sum(a)
100000 loops, best of 3: 16.5 us per loop
a = np.arange(100000, dtype=np.double)
%timeit np.einsum('i->', a)
10000 loops, best of 3: 103 us per loop
%timeit np.sum(a)
10000 loops, best of 3: 109 us per loop
Donc, vous avez essentiellement une surcharge de 3us presque constante lorsque vous appelez np.sum
plus de np.einsum
, ils fonctionnent donc aussi vite, mais il faut un peu plus de temps pour démarrer. Pourquoi est-ce possible? Mon argent est sur les éléments suivants:
a = np.arange(1000, dtype=object)
%timeit np.einsum('i->', a)
Traceback (most recent call last):
...
TypeError: invalid data type for einsum
%timeit np.sum(a)
10000 loops, best of 3: 20.3 us per loop
Je ne sais pas exactement ce qui se passe, mais il semble que np.einsum
ignore certaines vérifications pour extraire des fonctions spécifiques au type pour effectuer les multiplications et les ajouts, et va directement avec *
et +
pour les types C standard uniquement.
Les cas multidimensionnels ne sont pas différents:
n = 10; a = np.arange(n**3, dtype=np.double).reshape(n, n, n)
%timeit np.einsum('ijk->', a)
100000 loops, best of 3: 3.79 us per loop
%timeit np.sum(a)
100000 loops, best of 3: 7.33 us per loop
n = 100; a = np.arange(n**3, dtype=np.double).reshape(n, n, n)
%timeit np.einsum('ijk->', a)
1000 loops, best of 3: 1.2 ms per loop
%timeit np.sum(a)
1000 loops, best of 3: 1.23 ms per loop
Donc, une surcharge généralement constante, pas une course plus rapide une fois qu'ils s'y sont mis.
Une mise à jour pour numpy 1.16.4: les fonctions natives de Numpy sont plus rapides que les einsums dans presque tous les cas. Seules la variante externe d'einsum et le test sum23 sont plus rapides que les versions non einsum.
Si vous pouvez utiliser les fonctions natives de numpy, faites-le.
(Images créées avec perfplot , un de mes projets.)
Code pour reproduire les parcelles:
import numpy
import perfplot
def setup1(n):
return numpy.arange(n, dtype=numpy.double)
def setup2(n):
return numpy.arange(n ** 2, dtype=numpy.double).reshape(n, n)
def setup3(n):
return numpy.arange(n ** 3, dtype=numpy.double).reshape(n, n, n)
def setup23(n):
return (
numpy.arange(n ** 2, dtype=numpy.double).reshape(n, n),
numpy.arange(n ** 3, dtype=numpy.double).reshape(n, n, n)
)
def numpy_sum(a):
return numpy.sum(a)
def einsum_sum(a):
return numpy.einsum("ijk->", a)
perfplot.save(
"sum.png",
setup=setup3,
kernels=[numpy_sum, einsum_sum],
n_range=[2 ** k for k in range(10)],
logx=True,
logy=True,
title="sum",
)
def numpy_power(a):
return a * a * a
def einsum_power(a):
return numpy.einsum("ijk,ijk,ijk->ijk", a, a, a)
perfplot.save(
"power.png",
setup=setup3,
kernels=[numpy_power, einsum_power],
n_range=[2 ** k for k in range(9)],
logx=True,
logy=True,
)
def numpy_outer(a):
return numpy.outer(a, a)
def einsum_outer(a):
return numpy.einsum("i,k->ik", a, a)
perfplot.save(
"outer.png",
setup=setup1,
kernels=[numpy_outer, einsum_outer],
n_range=[2 ** k for k in range(13)],
logx=True,
logy=True,
title="outer",
)
def dgemm_numpy(a):
return numpy.dot(a, a)
def dgemm_einsum(a):
return numpy.einsum("ij,jk", a, a)
def dgemm_einsum_optimize(a):
return numpy.einsum("ij,jk", a, a, optimize=True)
perfplot.save(
"dgemm.png",
setup=setup2,
kernels=[dgemm_numpy, dgemm_einsum],
n_range=[2 ** k for k in range(13)],
logx=True,
logy=True,
title="dgemm",
)
def dot_numpy(a):
return numpy.dot(a, a)
def dot_einsum(a):
return numpy.einsum("i,i->", a, a)
perfplot.save(
"dot.png",
setup=setup1,
kernels=[dot_numpy, dot_einsum],
n_range=[2 ** k for k in range(20)],
logx=True,
logy=True,
title="dot",
)
def sum23_numpy(data):
a, b = data
return numpy.sum(a * b)
def sum23_einsum(data):
a, b = data
return numpy.einsum('ij,oij->', a, b)
perfplot.save(
"sum23.png",
setup=setup23,
kernels=[sum23_numpy, sum23_einsum],
n_range=[2 ** k for k in range(10)],
logx=True,
logy=True,
title="sum23",
)