Почему функция numpy einsum работает быстрее, чем встроенные функции numpy?

Давайте начнем с трех массивов dtype=np.double . Сроки выполняются на процессоре Intel с использованием numpy 1.7.1, скомпилированного с icc и связанного с mkl intel. Для проверки таймингов также использовался процессор AMD c numy 1.6.1, скомпилированный с gcc без mkl . Обратите внимание, что шкала таймингов почти линейно зависит от размера системы и не связана с небольшими накладными расходами, вызванными функциями numpy, if эти разности будут отображаться в микросекундах, а не в миллисекундах:

 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) 

Сначала рассмотрим функцию 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 

Полномочия:

 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 

Внешний продукт:

 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 

Все вышеперечисленное в два раза быстрее с np.einsum . Это должны быть яблоки для сравнения яблок, так как все конкретно dtype=np.double к dtype=np.double . Я ожидал бы ускорения в такой операции:

 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 по крайней мере вдвое быстрее для np.inner , np.outer , np.kron и np.sum независимо от выбора axes . Исключением является np.dot поскольку он вызывает DGEMM из библиотеки BLAS. Итак, почему np.einsum быстрее, чем другие функции numpy, которые эквивалентны?

Дело 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 

Ведущая теория – это комментарий от @sebergs, что np.einsum может использовать SSE2 , но ufuncs numpy не будут до numpy 1.8 (см. Журнал изменений ). Я считаю, что это правильный ответ, но он не смог его подтвердить. Некоторое ограниченное доказательство можно найти, изменив тип входного массива и наблюдая разницу в скорости и тот факт, что не все наблюдают одни и те же тенденции в таймингах.

3 Solutions collect form web for “Почему функция numpy einsum работает быстрее, чем встроенные функции numpy?”

Во-первых, в этом списке было много обсуждений. Например, см. 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

Некоторые из них сводятся к тому, что einsum является новым и, по-видимому, пытается улучшить einsum кеша и другие проблемы с доступом к памяти, в то время как многие из старых функций numpy сосредоточены на легко переносимой реализации на сильно оптимизированной. Я просто размышляю.


Однако некоторые из того, что вы делаете, – это не совсем сравнение яблок с яблоками.

В дополнение к тому, что уже сказал @Jamie, sum использует более подходящий аккумулятор для массивов

Например, sum более тщательно проверяет тип ввода и использует соответствующий аккумулятор. Например, рассмотрим следующее:

 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) 

Заметим, что sum верна:

 In [3]: x.sum() Out[3]: 25500 

Хотя einsum даст неправильный результат:

 In [4]: np.einsum('i->', x) Out[4]: 156 

Но если мы используем менее ограниченный dtype , мы все равно получим результат, который вы ожидаете:

 In [5]: y = 255 * np.ones(100) In [6]: np.einsum('i->', y) Out[6]: 25500.0 

Теперь, когда выпущен numpy 1.8, где согласно документам все ufuncs должны использовать SSE2, я хотел бы дважды проверить, что комментарий Seberg о SSE2 действителен.

Для выполнения теста была создана новая версия python 2.7 – numpy 1.7 и 1.8 были скомпилированы с icc используя стандартные опции на ядре AMD opteron, работающем под управлением Ubuntu.

Это тестовый запуск как до, так и после обновления 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 

Я думаю, что это довольно убедительно, что SSE играет большую роль в разнице во времени, следует отметить, что повторение этих тестов очень быстро, всего лишь на 0,003 секунды. Оставшаяся разница должна быть рассмотрена в других ответах на этот вопрос.

Я думаю, что эти тайминги объясняют, что происходит:

 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 

Таким образом, у вас в основном есть почти постоянная накладная 3us при вызове np.sum над np.einsum , поэтому они в основном работают так же быстро, но нужно немного больше времени, чтобы начать работу. Почему это может быть? Мои деньги заключаются в следующем:

 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 

Не уверен, что происходит точно, но кажется, что np.einsum пропускает некоторые проверки, чтобы извлечь специфические функции типа для выполнения умножений и дополнений и напрямую идет с * и + для стандартных типов C.


Многомерные случаи не различны:

 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 

Таким образом, в основном постоянные накладные расходы, а не более быстрый запуск, когда они приступят к нему.

  • Выбор встроенного языка
  • Ускорить сравнение поплавков между списками
  • Windows: медленный запуск приложения
  • Ускорьте Pandas cummin / cummax
  • Почему соединение происходит быстрее обычного конкатенации
  • Почему операции std :: string работают плохо?
  • Как повысить эффективность с помощью массивов numpy?
  • Почему буквальные форматированные строки настолько медленны в Python 3.6 alpha? (теперь фиксированная в 3.6 стабильной)
  • Python - лучший язык программирования в мире.