首页 > 解决方案 > 为什么 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

标签: pythonperformancenumpy

解决方案


这是由于内存访问和缓存。这些函数中的每一个都在做两件事,以第一个代码为例:

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- 它会自动执行其中的一些操作。我自己并没有太多使用它,但是如果中间数组会减慢您的程序速度,它应该可以提供很好的加速。


推荐阅读