Почему numy sum в 10 раз медленнее, чем оператор +? - PullRequest
0 голосов
/ 01 ноября 2018

Я заметил, что np.sum очень настойчиво в 10 раз медленнее, чем рукописная сумма.

np.sum с осью:

p1 = np.random.rand(10000, 2)
def test(p1):
    return p1.sum(axis=1)
%timeit test(p1)

186 мкс ± 4,21 мкс на цикл (среднее ± стандартное отклонение из 7 циклов, 1000 циклов в каждом)

np.sum без оси:

p1 = np.random.rand(10000, 2)
def test(p1):
    return p1.sum()
%timeit test(p1)

17,9 мкс ± 236 нс на цикл (среднее ± стандартное отклонение из 7 циклов, 10000 циклов в каждом)

+

p1 = np.random.rand(10000, 2)
def test(p1):
    return p1[:,0] + p1[:,1]
%timeit test(p1)

15,8 мкс ± 328 нс на цикл (среднее ± стандартное отклонение из 7 циклов, 100000 циклов каждый)

Умножение:

p1 = np.random.rand(10000, 2)
def test(p1):
    return p1[:,0]*p1[:,1]
%timeit test(p1)

15,7 мкс ± 701 нс на цикл (среднее ± стандартное отклонение из 7 циклов, 10000 циклов каждый)

Я не вижу причин для этого. Есть идеи почему? Моя версия NumPy 1.15.3.

EDIT: с 10000000:

np.sum (with axis): 202 ms (5 x)
np.sum (without axis): 12 ms
+ : 46 ms (1 x)
* : 44.3 ms 

Так что я думаю, что в некоторой степени есть игра наверху ...

Ответы [ 2 ]

0 голосов
/ 01 ноября 2018

Основное различие заключается в больших накладных расходах при расчете a.sum(axis=1). Расчет сокращения (в данном случае sum) не является тривиальным вопросом:

  • необходимо учитывать ошибки округления и, следовательно, использовать парное суммирование для его уменьшения.
  • тайлинг важен для больших массивов, поскольку он максимально использует доступный кеш
  • Для того чтобы иметь возможность использовать SIMD-инструкции / возможности выполнения внеочередных функций современных процессоров, необходимо рассчитать несколько строк параллельно

Я обсуждал вышеуказанные темы более подробно, например здесь и здесь .

Однако все это не нужно и не лучше наивного суммирования, если добавить только два элемента - вы получите тот же результат, но с гораздо меньшими затратами и быстрее.

Только для 1000 элементов затраты на вызов функциональности numpy, вероятно, выше, чем на самом деле выполнение этих 1000 сложений (или умножений в этом отношении, потому что на современных процессорах конвейерные сложения / умножения имеют одинаковую стоимость) - как вы можете видеть, что для 10 ^ 4 время работы только примерно в 2 раза выше, что является верным признаком того, что накладные расходы играют большую роль для 10 ^ 3! В этом ответе более подробно рассматривается влияние потерь служебной информации и кэша.

Давайте посмотрим на результат профилировщика, чтобы увидеть, справедлива ли приведенная выше теория (я использую perf):

Для a.sum(axis=1):

  17,39%  python   umath.cpython-36m-x86_64-linux-gnu.so       [.] reduce_loop
  11,41%  python   umath.cpython-36m-x86_64-linux-gnu.so       [.] pairwise_sum_DOUBLE
   9,78%  python   multiarray.cpython-36m-x86_64-linux-gnu.so  [.] npyiter_buffered_reduce_iternext_ite
   9,24%  python   umath.cpython-36m-x86_64-linux-gnu.so       [.] DOUBLE_add
   4,35%  python   python3.6                                   [.] _PyEval_EvalFrameDefault
   2,17%  python   multiarray.cpython-36m-x86_64-linux-gnu.so  [.] _aligned_strided_to_contig_size8_src
   2,17%  python   python3.6                                   [.] lookdict_unicode_nodummy
   ...

Издержки использования reduce_loop, pairwise_sum_DOUBLE доминируют.

Для a[:,0]+a[:,1]):

   7,24%  python   python3.6                                   [.] _PyEval_EvalF
   5,26%  python   python3.6                                   [.] PyObject_Mall
   3,95%  python   python3.6                                   [.] visit_decref
   3,95%  python   umath.cpython-36m-x86_64-linux-gnu.so       [.] DOUBLE_add
   2,63%  python   python3.6                                   [.] PyDict_SetDef
   2,63%  python   python3.6                                   [.] _PyTuple_Mayb
   2,63%  python   python3.6                                   [.] collect
   2,63%  python   python3.6                                   [.] fast_function
   2,63%  python   python3.6                                   [.] visit_reachab
   1,97%  python   python3.6                                   [.] _PyObject_Gen

Как и следовало ожидать: большие издержки Python играют большую роль, используется простой DOUBLE_add.


При звонке * меньше затрат a.sum()

  • за один раз reduce_loop вызывается не для каждой строки, а только один раз, что означает значительно меньшие накладные расходы.
  • новые результирующие массивы не создаются, больше нет необходимости записывать 1000 дублей в память.

, поэтому можно ожидать, что a.sum() быстрее (несмотря на то, что нужно добавить 2000, а не 1000 - но, как мы видели, это в основном накладные расходы и реальная работа - дополнения не являются отвечает за большую долю времени работы).


Получение данных с помощью команды:

perf record python run.py
perf report

и

#run.py
import numpy as np
a=np.random.rand(1000,2)

for _ in range(10000):
  a.sum(axis=1)
  #a[:,0]+a[:,1]
0 голосов
/ 01 ноября 2018

Хорошо для .sum () с осью против без, с осью должен генерировать массив с плавающей запятой до вашего ввода, с элементом для каждой строки. Это означает, что он должен вызвать lower () 10000 раз по оси = 1. Без аргумента оси он вычисляет сумму каждого элемента в единственном числе с плавающей запятой, что составляет всего один вызов для уменьшения с помощью плоского представления массива.

Я не уверен, почему функция добавления вручную работает быстрее, и мне не хочется копаться в исходном коде, но я думаю, что у меня есть довольно хорошее предположение Я полагаю, что это связано с необходимостью выполнять уменьшение по оси = 1 для каждой строки, поэтому необходимо 10 000 отдельных вызовов для уменьшения. В функции ручного добавления разделение оси выполняется только один раз при определении параметров функции "+", и затем каждый элемент столбцов разделения может добавляться вместе параллельно.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...