循环中的矢量化比numba jitted函数中的嵌套循环慢

时间:2019-09-13 04:48:44

标签: python performance numpy vectorization numba

因此,我正在尝试在@njit中将向量化和由numba支持的for循环相结合来提高性能(我目前正在使用numba 0.45.1)。令人失望的是,我发现它实际上比我的代码中的纯嵌套循环实现要慢。

这是我的代码:

import numpy as np
from numba import njit

@njit
def func3(arr_in, win_arr):
    n = arr_in.shape[0]
    win_len = len(win_arr)

    result = np.full((n, win_len), np.nan)

    alpha_arr = 2 / (win_arr + 1)

    e = np.full(win_len, arr_in[0])
    w = np.ones(win_len)

    two_index = np.nonzero(win_arr <= 2)[0][-1]+1
    result[0, :two_index] = arr_in[0]

    for i in range(1, n):
        w = w + (1-alpha_arr)**i
        e = e*(1-alpha_arr) + arr_in[i]
        result[i,:] = e /w

    return result

@njit
def func4(arr_in, win_arr):
    n = arr_in.shape[0]
    win_len = len(win_arr)

    result = np.full((n, win_len), np.nan)

    alpha_arr = 2 / (win_arr + 1)

    e = np.full(win_len, arr_in[0])
    w = np.ones(win_len)

    two_index = np.nonzero(win_arr <= 2)[0][-1]+1
    result[0, :two_index] = arr_in[0]

    for i in range(1, n):
        for col in range(len(win_arr)):
            w[col] = w[col] + (1-alpha_arr[col])**i
            e[col] = e[col]*(1-alpha_arr[col]) + arr_in[i]
            result[i,col] = e[col] /w[col]

    return result

if __name__ == '__main__':
    np.random.seed(0)
    data_size = 200000
    winarr_size = 1000

    data = np.random.uniform(0,1000, size = data_size)+29000
    win_array = np.arange(1, winarr_size+1)

    abc_test3= func3(data, win_array)
    abc_test4= func4(data, win_array)

    print(np.allclose(abc_test3, abc_test4, equal_nan = True))

我使用以下配置对这两个函数进行了基准测试:

(data_size,winarr_size) = (200000,100), (200000,200),(200000,1000), (200000,2000), (20000,10000), (2000,100000)

并发现,纯嵌套循环实现(func4)始终比混合向量化(func3的for循环实现更快(约快2%至5%)。


我的问题如下:

1)要进一步提高代码速度需要更改什么?

2)为什么函数的矢量化版本的计算时间随win_arr的大小线性增长?我认为矢量化应该做到这一点,以便无论矢量大小如何,操作速度都是恒定的,但是在这种情况下显然不成立。

3)是否有一般条件下矢量化运算的计算时间仍将随输入大小线性增长?

2 个答案:

答案 0 :(得分:3)

似乎您误解了“向量化”的含义。向量化意味着您编写的代码可以在标量数组上操作,即使它们是标量-但这只是代码的外观,与性能无关。

在Python / NumPy世界中,矢量化还具有这样的含义,即与循环代码相比,矢量化操作中循环的开销(通常)很多更小。但是,矢量化的代码仍然必须执行循环(即使已隐藏在库中)!

此外,如果您使用numba编写循环,numba将对其进行编译并创建执行(通常)与矢量化NumPy代码一样快的快速代码。这意味着在numba函数内部,矢量化和非矢量化代码之间没有明显的性能差异。

这样应该可以回答您的问题:

  

2)为什么函数的矢量化版本的计算时间随win_arr的大小线性增长?我认为矢量化应该做到这一点,以便无论矢量大小如何,操作速度都是恒定的,但是在这种情况下显然不成立。

它线性增长,因为它仍然需要迭代。在矢量化代码中,循环只是隐藏在库例程中。

  

3)是否有一般条件下矢量化运算的计算时间仍将随输入大小线性增长?

否。


您还询问了如何使它更快。

已经提到的注释可以并行处理:

import numpy as np
import numba as nb

@nb.njit(parallel=True)
def func6(arr_in, win_arr):
    n = arr_in.shape[0]
    win_len = len(win_arr)

    result = np.full((n, win_len), np.nan)

    alpha_arr = 2 / (win_arr + 1)

    e = np.full(win_len, arr_in[0])
    w = np.ones(win_len)

    two_index = np.nonzero(win_arr <= 2)[0][-1]+1
    result[0, :two_index] = arr_in[0]

    for i in range(1, n):
        for col in nb.prange(len(win_arr)):
            w[col] = w[col] + (1-alpha_arr[col])**i
            e[col] = e[col] * (1-alpha_arr[col]) + arr_in[i]
            result[i,col] = e[col] /w[col]

    return result

这使代码在我的计算机上更快(4核)。

但是,还有一个问题是您的算法可能在数值上不稳定。当您将(1-alpha_arr[col])**i提升至十万次幂时,它会在某些时候下溢:

>>> alpha = 0.01
>>> for i in [1, 10, 100, 1_000, 10_000, 50_000, 100_000, 200_000]:
...     print((1-alpha)**i)
0.99
0.9043820750088044
0.3660323412732292
4.317124741065786e-05
2.2487748498162805e-44
5.750821364590612e-219
0.0  # <-- underflow
0.0

答案 1 :(得分:1)

对于诸如pow,除法等复杂的数学运算,请三思。如果您可以通过简单的运算(例如乘法,加法和减法)来替换它们,那么始终值得一试。

请注意,将alpha与其自身重复乘以仅在代数上与直接使用幂运算进行代数相同。由于这是数值数学,因此结果可能会有所不同。

还要避免不必要的临时数组。

第一次尝试

@nb.njit(error_model="numpy",parallel=True)
def func5(arr_in, win_arr):
    #filling the whole array with NaNs isn't necessary
    result = np.empty((win_arr.shape[0],arr_in.shape[0]))
    for col in range(win_arr.shape[0]):
        result[col,0]=np.nan

    two_index = np.nonzero(win_arr <= 2)[0][-1]+1
    result[:two_index,0] = arr_in[0]

    for col in nb.prange(win_arr.shape[0]):
        alpha=1.-(2./ (win_arr[col] + 1.))
        alpha_exp=alpha

        w=1.
        e=arr_in[0]

        for i in range(1, arr_in.shape[0]):
            w+= alpha_exp
            e = e*alpha + arr_in[i]
            result[col,i] = e/w
            alpha_exp*=alpha

    return result.T

第二次尝试(避免下溢)

@nb.njit(error_model="numpy",parallel=True)
def func7(arr_in, win_arr):
    #filling the whole array with NaNs isn't necessary
    result = np.empty((win_arr.shape[0],arr_in.shape[0]))
    for col in range(win_arr.shape[0]):
        result[col,0]=np.nan

    two_index = np.nonzero(win_arr <= 2)[0][-1]+1
    result[:two_index,0] = arr_in[0]

    for col in nb.prange(win_arr.shape[0]):
        alpha=1.-(2./ (win_arr[col] + 1.))
        alpha_exp=alpha

        w=1.
        e=arr_in[0]

        for i in range(1, arr_in.shape[0]):
            w+= alpha_exp
            e = e*alpha + arr_in[i]
            result[col,i] = e/w

          if np.abs(alpha_exp)>=1e-308:
              alpha_exp*=alpha
          else:
              alpha_exp=0.

    return result.T

时间

%timeit abc_test3= func3(data, win_array)
7.17 s ± 45.9 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit abc_test4= func4(data, win_array)
7.13 s ± 13.3 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
#from MSeifert answer (parallelized)
%timeit abc_test6= func6(data, win_array)
3.42 s ± 153 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit abc_test5= func5(data, win_array)
1.22 s ± 22.4 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit abc_test7= func7(data, win_array)
238 ms ± 5.55 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)