2015-05-13 2 views
8

Я пытаюсь оптимизировать следующий цикл:Оптимизировать двойную петлю в питоне

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_opt) выполняет очень хорошо. Но мне интересно, почему numba-версия функции двойного цикла (numba2) приводит к более медленному времени выполнения. Я попытался использовать jit вместо autojit, указав типы аргументов, но это было хуже.

Я также заметил, что первый цикл на самом маленьком цикле медленнее, чем первый цикл на самой большой петле. Есть ли объяснения?

Является ли это, я уверен, что эта функция двойного цикла может быть улучшена много векторизации проблемы (например, this) или с помощью другого метода (на карте?), Но я немного запутался в этих методах.

В других частях моего кода я использовал методы numba и numpy slicing для замены всех явных циклов, но в этом конкретном случае я не могу его настроить.

Любые идеи?

EDIT

Спасибо за все ваши комментарии. Я работал немного по этой проблеме:

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. Новые идеи приветствуются!

+0

написать итератор, чтобы сделать работу, ее быстро, эффективно и потребляет меньше памяти, чем ваши двойные петли. –

ответ

1

Numba очень быстро находится в nopython mode, но с вашим кодом он должен вернуться в режим 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 

Обратите внимание, что в release notes есть упоминание о 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 
+0

Спасибо, не знал о шагах! Он работает хорошо и быстрее, чем реализация numba, но я немного запутался в этой формулировке. Чтение документа помогает мне понять в двумерном случае, но в этом случае я все еще неясен для меня. Например, если мне нужно сделать один и тот же расчет с помощью 'rho [ix, iz-1: iz + 3]', я должен изменить 'shape' на' (nx, ws, nz-ws + 1) ' , но «strides» и «rho_windowed» остаются прежними? И о 'j, ijk-> ik'' в' einsum', какие изменения мне нужно сделать? –

+0

@IpseLium, Да, я понимаю ваше замешательство. Однако вы правы в новой форме. Действительно, вам также нужно будет изменить ход и вызов «einsum». Но гораздо проще было бы сохранить функцию такой же, как в моем ответе, и называть ее как 'stridetrick_einsum (c, rho [2: -3,1: -1] .T, a [2: -3,2: -3] .T) '. Я не могу проверить это прямо сейчас, но он должен работать. –

2

Как указано в performance section на блоге континуума, autojit компилируется по времени, в то время как jit компилирует загодя:

Numba может составить только по времени с autojit декоратора, или впереди время с JIT декоратором

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

Но я задаюсь вопросом, почему версия Numba функции двойной петли (numba2) приводит к замедлению времени выполнения

Numba не увеличивает производительность произвольных вызовов функций. Хотя я не могу сказать точно, я предполагаю, что накладные расходы на компиляцию JIT перевешивают выгоду от ее выполнения (если есть какая-либо польза вообще).

Я также заметил, что цикл сначала на самом маленьком цикле медленнее, чем , зацикливая сначала на самой большой петле. Есть ли объяснения?

Это, вероятно, связано с cache miss. Двумерный массив выделяется как непрерывный кусок памяти размером 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 
+0

Я чувствую, что вы говорите, что autojit работает как HotSpot на Java, где во время выполнения определяется, какие функции стоит компилировать. В то время как, похоже, autojit создает скомпилированную функцию для каждого уникального набора типов аргументов, с которыми он вызван. То есть ключевым отличием является то, что autojit компилируется во всех экземплярах, тогда как HotSpot только компилирует часто используемые функции. – Dunes

6

Вы не в полной мере воспользоваться возможностями Numpy в. 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 ,

В моей системе с вашим примером ввода этот код работает более чем в 300 раз быстрее, чем ваша функция numpy. Согласно вашим таймингам, это будет на один или два порядка быстрее, чем numba.

+2

Как вы умножаете 'c' и' rho'? Их размеры не равны: (4,) и (1024,256). –

+0

К сожалению ... Я неправильно прочитал ваш код и понял, что он постоянный. Это сделает вещи медленнее, но это все еще выполнимо, я постараюсь переделать мое решение ... – Jaime

7

В основном вы выполняете 2D-свертку с небольшой модификацией, что ваше ядро ​​не реверсирует, как это делает обычная операция convolution. Так, в принципе, есть две вещей, которые мы должны сделать здесь, чтобы использовать signal.convolve2d решить наше дело -

  • фрагмента входного массива rho, чтобы выбрать его часть, которая используется в оригинальной невменяемой версии коды , Это будут входные данные для свертки.
  • Обратное ядро, 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 быстрее, чем невменяемым код и о 20x лучше, чем самый быстрый подход, основанный на numbanumba1.

Смежные вопросы