Оптимизация двойного цикла в python
Я пытаюсь оптимизировать следующий цикл :
def numpy(nx, nz, c, rho):
for ix in range(2, nx-3):
for iz in range(2, nz-3):
a[ix, iz] = sum(c*rho[ix-1:ix+3, iz])
b[ix, iz] = sum(c*rho[ix-2:ix+2, iz])
return a, b
Я пробовал различные решения и обнаружил, что использование numba для расчета суммы продукта приводит к улучшению производительности:
import numpy as np
import numba as nb
import time
@nb.autojit
def sum_opt(arr1, arr2):
s = arr1[0]*arr2[0]
for i in range(1, len(arr1)):
s+=arr1[i]*arr2[i]
return s
def numba1(nx, nz, c, rho):
for ix in range(2, nx-3):
for iz in range(2, nz-3):
a[ix, iz] = sum_opt(c, rho[ix-1:ix+3, iz])
b[ix, iz] = sum_opt(c, rho[ix-2:ix+2, iz])
return a, b
@nb.autojit
def numba2(nx, nz, c, rho):
for ix in range(2, nx-3):
for iz in range(2, nz-3):
a[ix, iz] = sum_opt(c, rho[ix-1:ix+3, iz])
b[ix, iz] = sum_opt(c, rho[ix-2:ix+2, iz])
return a, b
nx = 1024
nz = 256
rho = np.random.rand(nx, nz)
c = np.random.rand(4)
a = np.zeros((nx, nz))
b = np.zeros((nx, nz))
ti = time.clock()
a, b = numpy(nx, nz, c, rho)
print 'Time numpy : ' + `round(time.clock() - ti, 4)`
ti = time.clock()
a, b = numba1(nx, nz, c, rho)
print 'Time numba1 : ' + `round(time.clock() - ti, 4)`
ti = time.clock()
a, b = numba2(nx, nz, c, rho)
print 'Time numba2 : ' + `round(time.clock() - ti, 4)`
это приводит к
время numpy: 4.1595
время numba1: 0.6993
время numba2: 1.0135
использование версии numba функции sum (sum_opt) работает очень хорошо. Но мне интересно, почему версия numba функции двойного цикла (numba2) приводит к более медленному времени выполнения. Я попытался использовать jit вместо autojit, указав типы аргументов, но это было хуже.
Я также заметил, что цикл сначала на самом маленьком цикле медленнее, чем цикл сначала на самом большом цикле. Есть ли объяснение ?
ли это, я уверен, что эта функция двойного цикла может быть улучшена много векторизации проблемы (например,этой) или с помощью другого метода (map ?) но я немного запутался в этих методах.
в других частях моего кода я использовал методы нарезки numba и numpy для замены всех явных циклов, но в этом конкретном случае я не знаю, как его настроить.
какие идеи ?
редактировать
Спасибо за все ваши комментарии. Я немного поработал над этой проблемой:
import numba as nb
import numpy as np
from scipy import signal
import time
@nb.jit(['float64(float64[:], float64[:])'], nopython=True)
def sum_opt(arr1, arr2):
s = arr1[0]*arr2[0]
for i in xrange(1, len(arr1)):
s+=arr1[i]*arr2[i]
return s
@nb.autojit
def numba1(nx, nz, c, rho, a, b):
for ix in range(2, nx-3):
for iz in range(2, nz-3):
a[ix, iz] = sum_opt(c, rho[ix-1:ix+3, iz])
b[ix, iz] = sum_opt(c, rho[ix-2:ix+2, iz])
return a, b
@nb.jit(nopython=True)
def numba2(nx, nz, c, rho, a, b):
for ix in range(2, nx-3):
for iz in range(2, nz-3):
a[ix, iz] = sum_opt(c, rho[ix-1:ix+3, iz])
b[ix, iz] = sum_opt(c, rho[ix-2:ix+2, iz])
return a, b
@nb.jit(['float64[:,:](int16, int16, float64[:], float64[:,:], float64[:,:])'], nopython=True)
def numba3a(nx, nz, c, rho, a):
for ix in range(2, nx-3):
for iz in range(2, nz-3):
a[ix, iz] = sum_opt(c, rho[ix-1:ix+3, iz])
return a
@nb.jit(['float64[:,:](int16, int16, float64[:], float64[:,:], float64[:,:])'], nopython=True)
def numba3b(nx, nz, c, rho, b):
for ix in range(2, nx-3):
for iz in range(2, nz-3):
b[ix, iz] = sum_opt(c, rho[ix-2:ix+2, iz])
return b
def convol(nx, nz, c, aa, bb):
s1 = rho[1:nx-1,2:nz-3]
s2 = rho[0:nx-2,2:nz-3]
kernel = c[:,None][::-1]
aa[2:nx-3,2:nz-3] = signal.convolve2d(s1, kernel, boundary='symm', mode='valid')
bb[2:nx-3,2:nz-3] = signal.convolve2d(s2, kernel, boundary='symm', mode='valid')
return aa, bb
nx = 1024
nz = 256
rho = np.random.rand(nx, nz)
c = np.random.rand(4)
a = np.zeros((nx, nz))
b = np.zeros((nx, nz))
ti = time.clock()
for i in range(1000):
a, b = numba1(nx, nz, c, rho, a, b)
print 'Time numba1 : ' + `round(time.clock() - ti, 4)`
ti = time.clock()
for i in range(1000):
a, b = numba2(nx, nz, c, rho, a, b)
print 'Time numba2 : ' + `round(time.clock() - ti, 4)`
ti = time.clock()
for i in range(1000):
a = numba3a(nx, nz, c, rho, a)
b = numba3b(nx, nz, c, rho, b)
print 'Time numba3 : ' + `round(time.clock() - ti, 4)`
ti = time.clock()
for i in range(1000):
a, b = convol(nx, nz, c, a, b)
print 'Time convol : ' + `round(time.clock() - ti, 4)`
ваше решение очень элегантное Divakar, но я должен использовать эту функцию большое количество время в моем коде. Итак, для 1000 итераций это приводит к
время numba1: 3.2487
время numba2: 3.7012
время numba3: 3.2088
convol время : 22.7696
autojit
и jit
очень близки.
Однако, при использовании jit
, представляется важным указать все типы аргументов.
Я не знаю, есть ли способ указать типы аргументов в jit
декоратора, когда функция имеет несколько выходов. Кто-то ?
пока я не нашел другого решения, кроме использования numba. Новые идеи приветствуются !
4 ответов
Numba очень быстро, в nopython
режим но с вашим кодом он должен вернуться к object
режим, который намного медленнее. Вы можете видеть, что это происходит, если вы проходите nopython=True
до jit
оформителя.
он компилируется в nopython
режим (по крайней мере, в Numba версии 0.18.2), если вы передаете a
и b
в качестве аргументов:
import numba as nb
@nb.jit(nopython=True)
def sum_opt(arr1, arr2):
s = arr1[0]*arr2[0]
for i in range(1, len(arr1)):
s+=arr1[i]*arr2[i]
return s
@nb.jit(nopython=True)
def numba2(nx, nz, c, rho, a, b):
for ix in range(2, nx-3):
for iz in range(2, nz-3):
a[ix, iz] = sum_opt(c, rho[ix-1:ix+3, iz])
b[ix, iz] = sum_opt(c, rho[ix-2:ix+2, iz])
return a, b
обратите внимание, что в заметки есть упоминание о autojit
быть осужденным в пользу jit
.
очевидно, вы еще не удовлетворены. Так как насчет решения, основанного на stride_tricks
?
from numpy.lib.stride_tricks import as_strided
def stridetrick_einsum(c, rho, out):
ws = len(c)
nx, nz = rho.shape
shape = (nx-ws+1, ws, nz)
strides = (rho.strides[0],) + rho.strides
rho_windowed = as_strided(rho, shape, strides)
np.einsum('j,ijk->ik', c, rho_windowed, out=out)
a = np.zeros((nx, nz))
stridetrick_einsum(c, rho[1:-1,2:-3], a[2:-3,2:-3])
b = np.zeros((nx, nz))
stridetrick_einsum(c, rho[0:-2,2:-3], b[2:-3,2:-3])
более того, так как a
и b
очевидно, почти точно такие же, вы можете вычислить их за один раз, а затем скопировать значения:
a = np.zeros((nx, nz))
stridetrick_einsum(c, rho[:-1,2:-3], a[1:-3,2:-3])
b = np.zeros((nx, nz))
b[2:-3,2:-3] = a[1:-4,2:-3]
a[1,2:-3] = 0.0
вы в основном выполняете 2D свертку там, с небольшой модификацией, что ваше ядро не реверсируется, как обычно convolution
операция.
Итак, в основном, есть две вещи, которые нам нужно сделать здесь, чтобы использовать signal.convolve2d
, чтобы решить наши дела -
- нарезать входной массив
rho
чтобы выбрать часть, которая используется в исходной версии кода loopy. Это будут входные данные для свертка. - обратный ядра,
c
и кормить его вместе с нарезанным данныхsignal.convolve2d
.
обратите внимание, что это должно быть сделано для исчисления как a
и b
отдельно.
вот реализация -
import numpy as np
from scipy import signal
# Slices for convolutions to get a and b respectively
s1 = rho[1:nx-1,2:nz-3]
s2 = rho[0:nx-2,2:nz-3]
kernel = c[:,None][::-1] # convolution kernel
# Setup output arrays and fill them with convolution results
a = np.zeros((nx, nz))
b = np.zeros((nx, nz))
a[2:nx-3,2:nz-3] = signal.convolve2d(s1, kernel, boundary='symm', mode='valid')
b[2:nx-3,2:nz-3] = signal.convolve2d(s2, kernel, boundary='symm', mode='valid')
Если вам не нужны дополнительные нули вокруг границ выходных массивов, вы можете просто использовать выходы из signal.convolve2d
как они есть, что должно дополнительно повысить производительность.
во время выполнения тестов
In [532]: %timeit loop_based(nx, nz, c, rho)
1 loops, best of 3: 1.52 s per loop
In [533]: %timeit numba1(nx, nz, c, rho)
1 loops, best of 3: 282 ms per loop
In [534]: %timeit numba2(nx, nz, c, rho)
1 loops, best of 3: 509 ms per loop
In [535]: %timeit conv_based(nx, nz, c, rho)
10 loops, best of 3: 15.5 ms per loop
так, для фактического входного datasize, предлагаемые свертки подход, основанный около 100x
быстрее, чем код loopy и о 20x
лучше, чем самый быстрый numba
подход numba1
.
вы не используете в полной мере возможности numpy. The numpythonic способ решения вашей проблемы будет чем-то вроде:
cs = np.zeros((nx+1, nz))
np.cumsum(c*rho, axis=0, out=cs[1:])
aa = cs[5:, 2:-3] - cs[1:-4, 2:-3]
bb = cs[4:-1, 2:-3] - cs[:-5, 2:-3]
aa
теперь будет держать центральную, ненулевую часть вашего a
время:
>>> a[:5, :5]
array([[ 0. , 0. , 0. , 0. , 0. ],
[ 0. , 0. , 0. , 0. , 0. ],
[ 0. , 0. , 2.31296595, 2.15743042, 2.5853117 ],
[ 0. , 0. , 2.02697233, 2.83191016, 2.58819583],
[ 0. , 0. , 2.4086584 , 2.45175615, 2.19628507]])
>>>aa[:3, :3]
array([[ 2.31296595, 2.15743042, 2.5853117 ],
[ 2.02697233, 2.83191016, 2.58819583],
[ 2.4086584 , 2.45175615, 2.19628507]])
и аналогично для bb
и b
.
в моей системе, с вашим образцом ввода, этот код работает более 300x быстрее, чем ваш
как говорится в раздел в блоге continuum,autojit
компилирует как раз вовремя, в то время как jit
компилирует заранее:
Numba может скомпилировать just-in-time с декоратором autojit или впереди время с JIT декоратор
это означает, что во многих случаях autojit
означает, что компилятор может сделать более обоснованное предположение о коде, который он компилирует, и оптимизировать после этого. Я знаю, что just-in-time компиляция впереди время звучит противоречиво, но эй.
но мне интересно, почему версия numba функции двойного цикла (numba2) приводит к замедлению времени выполнения
Numba не увеличивает производительность произвольных вызовов функций. Хотя я не могу сказать наверняка, я предполагаю, что накладные расходы на компиляцию JIT перевешивают выгоду от этого (если есть какая-либо польза вообще).
Я также заметил, что цикл сначала на наименьший цикл медленнее, чем сначала петля На самой Большой петле. Есть ли объяснение ?
это, вероятно, из-за кэш Мисс. В 2-мерный массив выделяется как непрерывный кусок памяти размером rows * columns
. То, что извлекается в кэш, основано на комбинации временной (то, что недавно было использовано) и пространственной (то, что близко в памяти к тому, что было использовано) локальности, т. е. то, что считается используемым далее.
при переборе сначала по строкам выполняется итерация в том порядке, в котором данные отображаются в памяти. При итерации по столбцам сначала вы" пропускаете " ширину строки в памяти каждый раз, что делает менее вероятным, что место памяти, к которому вы обращаетесь, было извлечено в кэш.
2D array: [[1,2,3],[4,5,6],[7,8,9]]
In memory: 1 2 3 4 5 6 7 8 9
предположим, чрезмерно упрощенный, глупый алгоритм выборки кэша, который извлекает 3 последующих местоположения памяти.
итерация строки-первая:
In memory: 1 2 3 | 4 5 6 | 7 8 9
Accessed: 1 2 3 | 4 5 6 | 7 8 9
Cache miss: - - - | - - - | - - -
переборе первая колонка:
In memory: 1 2 3 | 4 5 6 | 7 8 9
Accessed: 1 4 7 | 2 5 8 | 3 6 9
Cache miss: - - - | x x x | x x x