I am trying to optimize the following loop:
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
I tried different solutions and found, using numba to calculate the amount of the product, leads to better results:
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)`
This will lead to
Numpy time: 4.1595
Numba1 time: 0.6993
Numba2 Time: 1.0135
Using the numba version of sum (sum_opt) is doing very well. But I am wondering why the numba version of the double loop function (numba2) leads to a slower runtime. I tried using jit instead of autojit by specifying argument types, but that was worse.
I also noticed that at first the cycle on the smallest cycle is slower than the cycle at first on the largest cycle. Is there any explanation?
I am sure that this double-loop function can be improved by a lot of vectorization of the problem (like this ) or by using another method (map?) But I'm a little confused about these methods.
In other parts of my code, I used the numba and numpy slicing methods to replace all explicit loops, but in this particular case, I cannot configure it.
Any ideas?
EDIT
Thanks for your comments. I worked a bit on this issue:
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)`
Your solution is very elegant Divakar, but I have to use this feature in my code a lot of times. So for 1000 iterations this leads to
Numba1 Time: 3.2487
Numba2 Time: 3.7012
Numba3 Time: 3.2088
Convolution Time: 22.7696
autojit and jit very close. However, when using jit seems important to specify all types of arguments.
I don't know if there is a way to specify argument types in the jit decorator when the function has multiple outputs. Someone?
At the moment, I have not found a solution other than using numba. New ideas are welcome!