Оптимальное (транслируемое) матричное деление в numy.Избегать временных массивов или нет? - PullRequest
6 голосов
/ 14 октября 2011

Numpy позволяет добавлять / умножать / делить матрицы разных размеров при условии соблюдения определенных правил вещания . Кроме того, создание временных массивов является серьезным препятствием для скорости numpy.

Следующие тимитные результаты меня удивляют ... что происходит?

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.

Ответы [ 2 ]

5 голосов
/ 14 октября 2011

Я думал, что 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-битных целых).Дополнительные затраты ~ 11 МБ накладываются на импорт импорта и т. Д.

Для возврата x / y в качестве нового массива (т.е. без x /= y) потребуется еще 55 МБ.

100 запусков f_dot: Мы создаем здесь временный массив, поэтому мы ожидаем увидеть 11 + 82 + 55 + 55 МБ или ~ 203 МБ использования памяти.И вот что мы видим ... enter image description here

100 прогонов f_no_dot: Если временный массив не создан, мы ожидаем пиковое использование памяти 11 + 82+ 55 МБ или 148 МБ ...
enter image description here ... это именно то, что мы видим.

Итак, x / y - это , а не , создающий дополнительный num x 2 временный массив для деления.

Таким образом, деление занимает немного больше времени, чем оноесли бы он работал на двух массивах одинакового размера.

100 запусков f_in_place: Если мы можем изменить x на месте, мы можем сэкономить еще больше памяти,если это главная проблема.enter image description here

По сути, в некоторых случаях numpy пытается сохранить память за счет скорости.

2 голосов
/ 15 октября 2011

То, что вы видите, - это, скорее всего, издержки на итерацию для небольшого измерения (2,).Numpy (версии <1.6) эффективно работал только с операциями, включающими <em>смежные массивы (той же формы).Эффект исчезает при увеличении размера последнего измерения.

Чтобы увидеть эффект смежности: 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 мс.)

Обратите внимание также, что временные данные не , что дорого:

<code>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 генерировал специализированный машинный код на лету для каждого цикла, ноне делает этого (по крайней мере пока:)

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