在过去的几天中,我一直在努力改善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情况下情况恰好相反。
答案 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}},并将速度下降归咎于implementation(npy_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的副本。尽管如此,从以上证据来看,%
似乎很可能是罪魁祸首。