Pourquoi l'einsum de num Py est-il plus rapide que les fonctions intégrées de num PY?

Permet de commencer avec trois tableaux de dtype=np.double . Les chronométrages sont effectués sur un processeur intel en utilisant numpy 1.7.1 compilé avec icc et lié au mkl d'intel . Un cpu AMD avec numpy 1.6.1 compilé avec gcc sans mkl a également été utilisé pour vérifier les chronométrages. S'il vous plaît noter l'échelle de temps presque linéairement avec la taille du système et ne sont pas en raison de la petite tête encourue dans les fonctions de numpy if déclarations ces différences apparaîtra en microsecondes pas 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 la fonction np.sum :

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

Powers:

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

toutes les réponses ci-dessus sont deux fois plus rapides avec np.einsum . Il devrait s'agir de comparaisons de pommes à pommes puisque tout est spécifiquement de dtype=np.double . J'attendrais la vitesse 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 indépendamment de la sélection axes . L'exception principale étant np.dot , comme il l'appelle DGEMM à partir d'une bibliothèque BLAS. Alors pourquoi np.einsum est plus rapide que les autres fonctions numpy qui sont équivalentes?

La DGEMM cas pour l'exhaustivité:

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

le leader de La théorie est de @sebergs comment que np.einsum peut faire usage de SSE2 , mais les ufuncs de numpy ne seront pas jusqu'à numpy 1.8 (voir le changer le journal ). Je crois que c'est la bonne réponse, mais ont pas été en mesure de 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 temps.

65
demandé sur Daniel 2013-08-21 22:31:42

3 réponses

tout d'abord, il y a eu beaucoup de discussions à ce sujet sur la liste des Nuls. Voir, par exemple,: 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

une partie de cela se résume au fait que einsum est nouveau, et essaie probablement d'être meilleur au sujet de cache l'alignement et d'autres problèmes d'accès à la mémoire, tandis que plusieurs des anciennes fonctions de numpy se concentrent sur une implémentation facilement portable plutôt que fortement optimisée. Je suis juste en spéculant, là, si.


cependant, ce que vous faites n'est pas tout à fait une comparaison "pomme à pomme".

en plus de ce que @Jamie a déjà dit, sum utilise un accumulateur plus approprié pour les tableaux

pour exemple, sum est plus prudent de vérifier le type de l'entrée et un accumulateur. Par exemple, considérons ce qui suit:

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)

noter que le 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
29
répondu Joe Kington 2013-08-21 19:27:56

maintenant que numpy 1.8 est sorti, où selon les docs tous les ufuncs 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és avec icc en utilisant les options standard sur un noyau opteron AMD tournant sous Ubuntu.

il s'agit de l'essai 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 c'est assez concluant que SSE joue un grand rôle dans les différences de timing, il convient de noter que la répétition de ces tests les timings Très par ~0.003 S. La différence restante devrait être couverte dans les autres réponses à cette question.

19
répondu Daniel 2013-10-26 21:51:16

je pense que ces minuteries 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 fondamentalement un 3US presque constant lorsque vous appelez np.sum au-dessus de np.einsum , donc ils courent essentiellement aussi vite, mais on prend un peu plus de temps pour se mettre en route. Pourquoi pourrait-il être? Mon argent est sur le suivant:

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

pas sûr de ce qui se passe exactement, mais il semble que np.einsum est sauter quelques vérifications pour extraire le type de fonctions spécifiques à faire les multiplications et les ajouts, et va directement avec * et + pour les types C standard seulement.


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 un overhead presque constant, pas un running plus rapide Une fois qu'ils y sont.

18
répondu Jaime 2013-08-21 19:29:08