小编典典

为什么在Python列表上执行“ for”要比在Numpy数组上执行“ for”更快?

python

因此,我没有讲一个很长的故事,而是在编写一些代码,即从二进制文件中读取一些数据,然后使用for循环遍历每个点。因此,我完成了代码,运行速度非常慢。我从约128个数据通道中遍历了约60,000个点,这需要一分钟或更长时间来处理。这比我预期的Python运行速度要慢得多。因此,我通过使用Numpy使整个事情变得更加高效,但是在试图弄清为什么原始进程运行如此缓慢的过程中,我们进行了一些类型检查,发现我遍历了Numpy数组而不是Python列表。好的,没有什么大不了的事情可以使我们测试设置的输入与我在循环之前将Numpy数组转换为列表一样。敲出运行一分钟所需的缓慢代码,现在花费了10秒。我被打倒了。我唯一的想法是将Numpy数组更改为Python列表,然后又将其更改回原来的状态,这又很慢。我简直不敢相信,所以我去找了更多确定的证据

$ python -m timeit -s "import numpy" "for k in numpy.arange(5000): k+1"
100 loops, best of 3: 5.46 msec per loop

$ python -m timeit "for k in range(5000): k+1"
1000 loops, best of 3: 256 usec per loop

到底是怎么回事?我知道Numpy数组和Python列表是不同的,但是为什么遍历数组中的每个点这么慢呢?

我相信在运行Numpy 10.1的Python 2.6和2.7中都观察到了这种行为。


阅读 222

收藏
2020-12-20

共1个答案

小编典典

我们可以花点时间找出答案:

>>> import numpy as np
>>> a = np.arange(32)
>>> a
array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16,
       17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31])
>>> a.data
<read-write buffer for 0x107d01e40, size 256, offset 0 at 0x107d199b0>
>>> id(a.data)
4433424176
>>> id(a[0])
4424950096
>>> id(a[1])
4424950096
>>> for item in a:
...   print id(item)
... 
4424950096
4424950120
4424950096
4424950120
4424950096
4424950120
4424950096
4424950120
4424950096
4424950120
4424950096
4424950120
4424950096
4424950120
4424950096
4424950120
4424950096
4424950120
4424950096
4424950120
4424950096
4424950120
4424950096
4424950120
4424950096
4424950120
4424950096
4424950120
4424950096
4424950120
4424950096
4424950120

那么这是怎么回事?首先,我看了数组的内存缓冲区的内存位置。在4433424176。这本身并 不太有
启发性。但是,numpy将其数据存储为连续的C数组,因此numpy数组中的第一个元素 应对 应于数组本身的内存地址,但不这样:

>>> id(a[0])
4424950096

但这不是一件好事,因为那会破坏python中的不变性,即2个对象id在其生命周期内永远不会具有相同的对象。

那么,numpy如何做到这一点?好吧,答案是numpy必须将返回的对象包装为python类型(例如numpy.float64numpy.int64在这种情况下为),如果您逐项迭代1,则会花费时间。迭代时将进一步证明这一点-
我们看到,在数组上进行迭代时,我们在2个独立的ID之间交替。这意味着python的内存分配器和垃圾收集器正在超时工作以创建新对象,然后释放它们。

一个 名单 没有这个内存分配器/垃圾收集开销。列表中的对象已经作为python对象存在(并且它们在迭代后仍将存在),因此在列表的迭代中都没有任何作用。

时序方法:

另请注意,您的时间安排会因您的假设而有所偏离。您以为k + 1在两种情况下这应该花费相同的时间,但是事实并非如此。请注意,如果我重复您的时间 没有
做任何加法:

mgilson$ python -m timeit -s "import numpy" "for k in numpy.arange(5000): k"
1000 loops, best of 3: 233 usec per loop
mgilson$ python -m timeit "for k in range(5000): k"
10000 loops, best of 3: 114 usec per loop

只有大约2倍的差异。但是,执行加法运算会导致相差约5倍:

mgilson$ python -m timeit "for k in range(5000): k+1"
10000 loops, best of 3: 179 usec per loop
mgilson$ python -m timeit -s "import numpy" "for k in numpy.arange(5000): k+1"
1000 loops, best of 3: 786 usec per loop

为了好玩,让我们做一下添加:

$ python -m timeit -s "v = 1" "v + 1"
10000000 loops, best of 3: 0.0261 usec per loop
mgilson$ python -m timeit -s "import numpy; v = numpy.int64(1)" "v + 1"
10000000 loops, best of 3: 0.121 usec per loop

最后,您的时间还包括不理想的列表/数组构建时间:

mgilson$ python -m timeit -s "v = range(5000)" "for k in v: k"
10000 loops, best of 3: 80.2 usec per loop
mgilson$ python -m timeit -s "import numpy; v = numpy.arange(5000)" "for k in v: k"
1000 loops, best of 3: 237 usec per loop

注意,在这种情况下,numpy实际上离列表解决方案还很远。这表明 迭代 确实
慢一些,如果将numpy类型转换为标准python类型,则可能会加快速度。

1注意,切片时不会花费很多时间,因为那只需要分配O(1)新对象,因为numpy将 视图 返回到原始数组。

2020-12-20