【问题标题】:Why is a `for` over a Python list faster than over a Numpy array?为什么 Python 列表上的“for”比 Numpy 数组上的更快?
【发布时间】:2016-05-15 22:01:18
【问题描述】:

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

【问题讨论】:

  • 你用的是什么 Python 版本?
  • 不是 numpy 专家,但我认为关键是您通常不希望使用 python 循环迭代 numpy 数组,因为那样会失去速度提升(实际上更多,因为开销)。
  • 冒险猜测 - numpy 必须从 C 到 Python 才能产生它的值,其中 Python for 循环非常理想。你可以考虑看看dis.dis(some_func) 看看那里有没有什么。
  • numpy.arange 正在创建一个 numpy 数组以及随之而来的所有开销......(事实上它通常操作起来更快......)

标签: python arrays performance numpy


【解决方案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的内存分配器和垃圾收集器正在超时工作以创建新对象然后释放它们。

list 没有这种内存分配器/垃圾收集器开销。列表中的对象已经作为 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 实际上离列表解决方案更远了。这表明 iteration 确实 较慢,如果将 numpy 类型转换为标准 python 类型,您可能会获得一些加速。

1注意,切片时这不会花费很多时间,因为只需要分配 O(1) 个新对象,因为 numpy 返回一个 view 进入原始数组。

【讨论】:

  • 是的,我相信它似乎主要包含在数据转换中。我通常在这样的测试时+1,因为我不确定 k 是否会被优化掉。标记为已回答
  • 我在stackoverflow.com/a/34273109/901925 中证明a[0] 具有array 的所有属性。还有其他一些 SO 问题询问哪个更快,是列表还是数组。
  • @mechsin -- k 不会被优化掉 -- 但即使是这样,只要没有迭代,这也不会是一个大问题。并且迭代无法被优化掉,因为python没有办法先验地知道迭代项目是否有副作用(例如,使用生成器与不可消耗的可迭代对象,例如列表)。
【解决方案2】:

使用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 微秒

假设您有两个任意值的列表,您希望将每个列表相乘。在 vanilla 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 数组的真正目的。这是比较飞机和汽车;是的,飞机的速度通常更快,但尝试飞往当地的超市并不谨慎。

【讨论】:

  • 我通过使用向量化和 Numpy 数组优化了慢代码,所以我很清楚这是有目的的。我真的想问我问的问题,这就是为什么循环遍历两个本质上相同的对象如此不同。我认为 mgilson 可能是正确的,它与数据转换有关。
猜你喜欢
  • 2018-05-29
  • 2014-04-08
  • 1970-01-01
  • 2020-12-31
  • 2011-03-21
  • 2019-07-24
  • 2018-09-20
相关资源
最近更新 更多