繁体   English   中英

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

[英]Why is a `for` over a Python list faster than over a Numpy array?

所以没有讲一个很长的故事,我正在编写一些代码,我从二进制文件中读取一些数据,然后使用 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 中都观察到了这种行为。

我们可以做一些调查来解决这个问题:

>>> 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

最后,您的 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

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

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

使用 python 2.7

这是我的速度和 xrange:

python -m timeit -s "import numpy" "for k in numpy.arange(5000): k+1"

1000 个循环,最好的 3 个:每个循环 1.22 毫秒

python -m timeit "for k in range(5000): k+1"

10000 个循环,最好的 3 个:每个循环 186 微秒

python -m timeit "for k in xrange(5000): k+1"

10000 个循环,最好的 3 个:每个循环 161 微秒


Numpy 明显更慢,因为它迭代特定于 numpy 的数组。 这不是其主要预期功能。 在许多情况下,它们应该更像是一个整体的数字集合,而不是简单的列表/可迭代对象。 例如,如果我们有一个相当大的 Python 数字列表,我们想对其进行三次幂运算,我们可能会这样做:

python -m timeit "lst1 = [x for x in range(100000)];" "lst2 = map(lambda x: x**3, lst1)"

10 个循环,最好的 3 个:每个循环 125 毫秒

注意:lst1 代表一个任意列表。 我知道您可以通过对范围内的 x 执行 x**3 来加快原始 lambda 的速度,但这与应该已经存在并且很可能不是顺序的列表不一致。

无论如何, numpy 应该被视为一个数组:

python -m timeit -s "import numpy" "lst1 = numpy.arange(100000)" "lst2 = lst1**2"

10000 个循环,最好的 3 个:每个循环 120 微秒

假设您有两个任意值列表,您希望将每个值相乘。 在香草 python 中,你可以这样做:

python -m timeit -s "lst1 = [x for x in xrange(0, 10000, 2)]" "lst2 = [x for x in xrange(2, 10002, 2)]" "lst3 = [x*y for x,y in zip(lst1, lst2)]"

1000 个循环,最好的 3 个:每个循环 736 微秒

在 Numpy 中:

python -m timeit -s "import numpy" "lst1 = numpy.arange(0, 10000, 2)" "lst2 = numpy.arange(2, 10002, 2)" "lst3 = lst1*lst2"

100000 个循环,最好的 3 个:每个循环 10.9 微秒

在这最后两个例子中,NumPy 作为明显的赢家一飞冲天。 对于列表的简单迭代,range 或 xrange 就完全足够了,但是您的示例没有考虑 Numpy 数组的真正目的。 它在比较飞机和汽车; 是的,飞机通常比他们打算做的更快,但是尝试飞往当地的超市并不谨慎。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM