为什么在Python列表上的"for"比在Numpy数组上更快?

mec*_*sin 11 python arrays performance numpy

所以我没有讲一个很长的故事,我正在研究一些代码,我从二进制文件中读取一些数据,然后使用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
Run Code Online (Sandbox Code Playgroud)

到底是怎么回事?我知道Numpy数组和Python列表是不同的,但为什么迭代数组中的每个点都要慢得多?

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

mgi*_*son 17

我们可以做一点调查来弄清楚这一点:

>>> 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
Run Code Online (Sandbox Code Playgroud)

那么这里发生了什么?首先,我看一下数组内存缓冲区的内存位置.它在4433424176.这本身并不太具启发性.但是,numpy将它的数据存储为连续的C数组,因此numpy数组中的第一个元素应该对应于数组本身的内存地址,但它不会:

>>> id(a[0])
4424950096
Run Code Online (Sandbox Code Playgroud)

并且它不是一件好事,因为这会破坏python中的不变量,即2个对象id在其生命周期中永远不会相同.

那么,numpy如何实现这一目标呢?那么,答案是numpy的有包装用Python类型(如返回的对象numpy.float64或者numpy.int64在这种情况下),这需要一定的时间,如果你迭代项目,通过项目1.迭代时会进一步证明这一点 - 我们看到在迭代数组时我们在两个独立的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
Run Code Online (Sandbox Code Playgroud)

这只差不多是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
Run Code Online (Sandbox Code Playgroud)

为了好玩,我们只需要添加:

$ 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
Run Code Online (Sandbox Code Playgroud)

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

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
Run Code Online (Sandbox Code Playgroud)

请注意,在这种情况下,numpy实际上远离列表解决方案.这表明,迭代确实慢,如果你转换numpy的类型标准Python类型,你可能会得到一些速度提升.

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