numpy数组上的余数函数(%)运行时间远比手动余数计算长

时间:2018-09-09 08:47:21

标签: python performance numpy

在过去的几天中,我一直在努力改善python函数的运行时,该函数需要除其他功能外还多次使用剩余函数(%)。我的主要测试用例是超过80,000个元素的numpy数组(单调递增),具有10000次迭代,尽管我也尝试了其他各种大小。

最终,我到达了余数函数成为主要瓶颈的地步,并尝试了各种解决方案。这是我在运行以下代码时发现的行为:

import numpy as np
import time

a = np.random.rand(80000)
a = np.cumsum(a)
d = 3
start_time1 = time.time()
for i in range(10000):
    b = a % d
    d += 0.001
end_time1 = time.time()
d = 3
start_time2 = time.time()
for i in range(10000):
    b = a - (d * np.floor(a / d))
    d += 0.001
end_time2 = time.time()
print((end_time1 - start_time1) / 10000)
print((end_time2 - start_time2) / 10000)

输出为:

0.0031344462633132934
0.00022937238216400147

当数组大小增加到800,000时:

0.014903099656105041
0.010498356819152833

(对于这篇文章,我只为实际输出运行了一次代码,同时试图理解问题,我始终如一地得到这些结果。)

这解决了我的运行时问题-我很难理解为什么。我想念什么吗?我能想到的唯一区别是附加函数调用的开销,但是第一种情况非常极端(并且1.5倍的运行时也不够好),如果是这种情况,我会认为存在np.remainder函数毫无意义。

编辑: 我尝试使用非numpy循环测试相同的代码:

import numpy as np
import time


def pythonic_remainder(array, d):
    b = np.zeros(len(array))
    for i in range(len(array)):
        b[i] = array[i] % d

def split_pythonic_remainder(array, d):
    b = np.zeros(len(array))
    for i in range(len(array)):
        b[i] = array[i] - (d * np.floor(array[i] / d))

def split_remainder(a, d):
    return a - (d * np.floor(a / d))

def divide(array, iterations, action):
    d = 3
    for i in range(iterations):
        b = action(array, d)
        d += 0.001

a = np.random.rand(80000)
a = np.cumsum(a)
start_time = time.time()
divide(a, 10000, split_remainder)
print((time.time() - start_time) / 10000)

start_time = time.time()
divide(a, 10000, np.remainder)
print((time.time() - start_time) / 10000)
start_time = time.time()
divide(a, 10000, pythonic_remainder)
print((time.time() - start_time) / 10000)

start_time = time.time()
divide(a, 10000, split_pythonic_remainder)
print((time.time() - start_time) / 10000)

我得到的结果是:

0.0003770533800125122
0.003932329940795899
0.018835473942756652
0.10940513386726379

我发现有趣的是,在非numpy情况下情况恰好相反。

1 个答案:

答案 0 :(得分:11)

我最好的假设是,您的NumPy安装正在fmod计算中使用未经优化的%。这就是为什么。


首先,我无法在NumPy 1.15.1的常规点子安装版本上重现您的结果。我只能得到大约10%的性能差异(asdf.py包含您的计时代码):

$ python3.6 asdf.py
0.0006543657302856445
0.0006025806903839111

可以通过从NumPy Git存储库的克隆中手动构建(v {1.1}(python3.6 setup.py build_ext --inplace -j 4)来再现主要的性能差异,

$ python3.6 asdf.py
0.00242799973487854
0.0006397026300430298

这表明我的点子安装版本%比手动构建或您已安装的版本进行了更好的优化。


在幕后看,很容易在NumPy中查看浮点%的{​​{3}},并将速度下降归咎于implementationnpy_divmod@c@会计算{ {1}}和//):

%

但是在我的实验中,删除floordiv并没有带来任何好处。对于编译器来说,优化似乎很容易,所以也许它已经被优化了,或者也许它只是运行时间的一小部分。

让我们只关注NPY_NO_EXPORT void @TYPE@_remainder(char **args, npy_intp *dimensions, npy_intp *steps, void *NPY_UNUSED(func)) { BINARY_LOOP { const @type@ in1 = *(@type@ *)ip1; const @type@ in2 = *(@type@ *)ip2; npy_divmod@c@(in1, in2, (@type@ *)op1); } } 中的一行,而不是floordiv:npy_divmod@c@

fmod

这是初始的余数计算,在特殊情况下处理并调整结果以匹配右侧操作数的符号。如果在我的手动构建中比较mod = npy_fmod@c@(a, b); %的性能:

numpy.fmod

我们看到>>> import timeit >>> import numpy >>> a = numpy.arange(1, 8000, dtype=float) >>> timeit.timeit('a % 3', globals=globals(), number=1000) 0.3510419335216284 >>> timeit.timeit('numpy.fmod(a, 3)', globals=globals(), number=1000) 0.33593094255775213 >>> timeit.timeit('a - 3*numpy.floor(a/3)', globals=globals(), number=1000) 0.07980139832943678 似乎负责fmod的几乎整个运行时间。


我没有分解生成的二进制文件,也没有在指令级调试器中逐步查看生成的二进制文件,以查看确切的执行内容,当然,我无权访问您的机器或您的NumPy的副本。尽管如此,从以上证据来看,%似乎很可能是罪魁祸首。