#python #performance #numpy
#python #Производительность #numpy
Вопрос:
Numpy позволяет добавлять / умножать / делить матрицы разных размеров при условии соблюдения определенных правил вещания. Кроме того, создание временных массивов является основным препятствием для numpy.
Следующие результаты timit меня удивляют…что происходит?
In [41]: def f_no_dot(mat,arr):
....: return mat/arr
In [42]: def f_dot(mat,arr):
....: denominator = scipy.dot(arr, scipy.ones((1,2)))
....: return mat/denominator
In [43]: mat = scipy.rand(360000,2)
In [44]: arr = scipy.rand(360000,1)
In [45]: timeit temp = f_no_dot(mat,arr)
10 loops, best of 3: 44.7 ms per loop
In [46]: timeit temp = f_dot(mat,arr)
100 loops, best of 3: 10.1 ms per loop
Я думал, что f_dot будет медленнее, поскольку он должен был создать знаменатель временного массива, и я предположил, что этот шаг был пропущен f_no_dot . Я должен отметить, что эти времена масштабируются линейно (с размером массива, длиной до 1 миллиарда) для f_no_dot и немного хуже, чем линейный для f_dot.
Ответ №1:
Я думал, что f_dot будет медленнее, поскольку он должен был создать знаменатель временного массива, и я предположил, что этот шаг был пропущен f_no_dot .
Как бы то ни было, создание временного массива пропускается, поэтому f_no_dot
выполняется медленнее (но использует меньше памяти).
Поэлементные операции с массивами одинакового размера Выполняются быстрее, потому что numpy не нужно беспокоиться о размерах (размерах, размере и т. Д.) массивов.
Операции, использующие широковещательную передачу, обычно будут немного медленнее, чем операции, которые этого не делают.
Если у вас есть свободная память, создание временной копии может ускорить работу, но потребует больше памяти.
Например, сравнивая эти три функции:
import numpy as np
import timeit
def f_no_dot(x, y):
return x / y
def f_dot(x, y):
denom = np.dot(y, np.ones((1,2)))
return x / denom
def f_in_place(x, y):
x /= y
return x
num = 3600000
x = np.ones((num, 2))
y = np.ones((num, 1))
for func in ['f_dot', 'f_no_dot', 'f_in_place']:
t = timeit.timeit('%s(x,y)' % func, number=100,
setup='from __main__ import x,y,f_dot, f_no_dot, f_in_place')
print func, 'time...'
print t / 100.0
Это дает аналогичные тайминги для ваших результатов:
f_dot time...
0.184361531734
f_no_dot time...
0.619203259945
f_in_place time...
0.585789341927
Однако, если мы сравним использование памяти, все станет немного понятнее…
Общий размер нашего x
и y
массивов составляет около 27,5 55 МБ, или 82 МБ (для 64-разрядных целых чисел). При импорте numpy дополнительные ~ 11 МБ накладных расходов и т. Д.
Для возврата x / y
в виде нового массива (т. Е. Не выполнения x /= y
) потребуется еще 55 МБ массива.
100 запусков f_dot
: мы создаем здесь временный массив, поэтому мы ожидаем увидеть 11 82 55 55 МБ или ~ 203 МБ используемой памяти. И это то, что мы видим…
100 запусков f_no_dot
: если временный массив не создан, мы ожидаем, что максимальное использование памяти составит 11 82 55 МБ, или 148 МБ …
… это именно то, что мы видим.
Итак, x / y
не создается дополнительный num x 2
временный массив для выполнения деления.
Таким образом, разделение занимает немного больше времени, чем если бы оно работало с двумя массивами одинакового размера.
100 запусков f_in_place
: если мы можем модифицировать x
на месте, мы можем сэкономить еще больше памяти, если это главная проблема.
В основном, numpy пытается сэкономить память за счет скорости, в некоторых случаях.
Комментарии:
1. Спасибо, Джо! Вопрос: Итак, как numpy использует временный массив для ускорения работы? Моя теория: при делении x / denom он может делить обе строки одновременно, используя какую-то многопоточность. Я прав?
2. На самом деле многопоточности нет. Просто проще работать с чем-то поэлементно, если массивы имеют одинаковый размер. В принципе, чтобы обобщить вещи для N-измерений, во внутреннем цикле при использовании широковещательной передачи существует довольно сложная логика. Если это не так, то вы просто делите каждый элемент. Если бы вы написали код для конкретного случая работы между 2D-массивом и 1D-массивом, тогда это было бы так же быстро. Я недостаточно знаю о внутренних компонентах, но я думаю, что некоторые из новых итераторов «под капотом» в numpy должны ускорить некоторые из таких операций…
Ответ №2:
То, что вы видите, скорее всего, является накладными расходами на итерации при небольшом (2,)
измерении. Numpy (версии <1.6) эффективно справлялся только с операциями, связанными со смежными массивами (одинаковой формы). Эффект исчезает по мере увеличения размера последнего измерения.
Чтобы увидеть эффект смежности:
In [1]: import numpy
In [2]: numpy.__version__
Out[2]: '1.5.1'
In [3]: arr_cont1 = numpy.random.rand(360000, 2)
In [4]: arr_cont2 = numpy.random.rand(360000, 2)
In [5]: arr_noncont = numpy.random.rand(360000, 4)[:,::2]
In [6]: arr_bcast = numpy.random.rand(360000, 1)
In [7]: %timeit arr_cont1 / arr_cont2
100 loops, best of 3: 5.75 ms per loop
In [8]: %timeit arr_noncont / arr_cont2
10 loops, best of 3: 54.4 ms per loop
In [9]: %timeit arr_bcast / arr_cont2
10 loops, best of 3: 55.2 ms per loop
Однако ситуация значительно улучшилась в Numpy> = 1.6.0:
In [1]: import numpy In [2]: numpy.__version__ Out[2]: '1.6.1' In [3]: arr_cont1 = numpy.random.rand(360000, 2) In [4]: arr_cont2 = numpy.random.rand(360000, 2) In [5]: arr_noncont = numpy.random.rand(360000, 4)[:,::2] In [6]: arr_bcast = numpy.random.rand(360000, 1) In [7]: %timeit arr_cont1 / arr_cont2 100 loops, best of 3: 5.37 ms per loop In [8]: %timeit arr_noncont / arr_cont2 100 loops, best of 3: 6.12 ms per loop In [9]: %timeit arr_bcast / arr_cont2 100 loops, best of 3: 7.81 ms per loop
(Все приведенные выше тайминги, вероятно, с точностью до 1 мс.)
Обратите также внимание, что временные ресурсы не такие дорогие:
In [82]: %timeit arr_cont1.copy()
1000 loops, best of 3: 778 us per loop
РЕДАКТИРОВАТЬ: обратите внимание выше, что это также arr_noncont
является своего рода смежным с шагом 2*itemsize
, так что внутренний цикл можно распутать - Numpy может сделать это примерно так же быстро, как непрерывный массив. При широковещательной передаче (или с действительно несмежным массивом, таким как numpy.random.rand(360000*2, 2)[::2,:]
, внутренний цикл не может быть распутан, и эти случаи немного медленнее. Улучшение этого все равно было бы возможно, если бы Numpy создавал адаптированный машинный код "на лету" для каждого цикла, но он этого не делает (по крайней мере, пока 🙂