python - 为什么 NumPy 有时比 NumPy + 普通 Python 循环慢?
问题描述
这是基于2018-10 提出的这个问题。
考虑以下代码。三个简单的函数来计算 NumPy 3D 数组 (1000 × 1000 × 1000) 中的非零元素。
import numpy as np
def f_1(arr):
return np.sum(arr > 0)
def f_2(arr):
ans = 0
for val in range(arr.shape[0]):
ans += np.sum(arr[val, :, :] > 0)
return ans
def f_3(arr):
return np.count_nonzero(arr)
if __name__ == '__main__':
data = np.random.randint(0, 10, (1_000, 1_000, 1_000))
print(f_1(data))
print(f_2(data))
print(f_3(data))
我机器上的运行时(Python 3.7.?、Windows 10、NumPy 1.16.?):
%timeit f_1(data)
1.73 s ± 21.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit f_2(data)
1.4 s ± 1.36 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit f_3(data)
2.38 s ± 956 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)
因此,比和f_2()
工作得更快。但是,尺寸较小的情况并非如此。问题是——为什么会这样?是 NumPy、Python 还是其他东西?f_1()
f_3()
data
解决方案
这是由于内存访问和缓存。这些函数中的每一个都在做两件事,以第一个代码为例:
np.sum(arr > 0)
它首先进行比较以查找arr
大于零(或非零,因为arr
包含非负整数)的位置。这将创建一个与 形状相同的中间数组arr
。然后,它对这个数组求和。
直截了当,对吧?好吧,使用时np.sum(arr > 0)
这是一个大数组。当它大到不适合缓存时,性能会下降,因为当处理器开始执行时,大多数数组元素将被从内存中逐出并需要重新加载。
由于f_2
迭代第一个维度,它正在处理较小的子数组。完成了相同的复制和求和,但这次中间数组适合内存。它在不留记忆的情况下被创建、使用和销毁。这要快得多。
现在,您会认为这f_3
将是最快的(使用内置方法和所有方法),但查看源代码表明它使用以下操作:
a_bool = a.astype(np.bool_, copy=False)
return a_bool.sum(axis=axis, dtype=np.intp
a_bool
只是另一种查找非零条目的方法,并创建一个大的中间数组。
结论
经验法则就是这样,而且经常是错误的。如果您想要更快的代码,请对其进行分析并查看问题所在(在此处进行了很好的工作)。
Python
有些事情做得很好。在优化的情况下,它可以比numpy
. 不要害怕将普通的旧 python 代码或数据类型与 numpy 结合使用。
如果您发现自己经常手动编写 for 循环以获得更好的性能,您可能想看看numexpr
- 它会自动执行其中的一些操作。我自己并没有太多使用它,但是如果中间数组会减慢您的程序速度,它应该可以提供很好的加速。
推荐阅读
- java - 如何在操作系统版本 19 上为支付网关生成访问令牌
- istanbul - 重命名伊斯坦布尔覆盖率报告 xml 文件
- javascript - JSX 中的 Vue v-on:click.native?
- javascript - vanilla js 相当于通过 div id 通过 XMLHttpRequest 的“加载页面片段”
- r - dplyr 使用两列作为自定义函数的参数来改变多列
- redis - 收到错误 redis.clients.jedis.exceptions.JedisMovedDataException: MOVED
- python - 'NumpyArrayWrapper' 对象不可迭代,当使用 pickle.load(f)
- 3d - L-tree Framework Mesh 生成
- terminal - 如何在 IPython shell 上更改 iTerm2 的颜色?
- java - 在 if 块中,我的 for 循环不起作用,因此 1,2,32 被烘烤而 3 没有