使用 Python 列表作为队列的效率

Posted

技术标签:

【中文标题】使用 Python 列表作为队列的效率【英文标题】:Efficiency of using a Python list as a queue 【发布时间】:2010-11-20 18:10:14 【问题描述】:

一位同事最近编写了一个程序,其中他使用 Python 列表作为队列。也就是说,他在需要插入项目时使用.append(x),在需要删除项目时使用.pop(0)

我知道 Python 有 collections.deque,我正试图弄清楚是否要花费我(有限的)时间来重写此代码以使用它。假设我们执行了数以百万计的追加和弹出操作,但从来没有超过几千个条目,那么他的列表使用会不会有问题?

特别是,Python 列表实现使用的底层数组是否会继续无限增长,即使列表只有一千个东西,也会有数百万个点,还是 Python 最终会做一个 realloc 并释放一些内存?

【问题讨论】:

底层证券不会无限期地继续增长(只会比它的“高水位线”大一点)。但某些答案中强调的 O(N) 与 O(1) 问题可能很重要。 【参考方案1】:

每个.pop(0) 需要 N 步,因为必须重新组织列表。所需的内存不会无休止地增长,只会与所持有的项目所需的一样大。

我建议使用deque 来获得 O(1) 的附加并从前面弹出。

【讨论】:

【参考方案2】:

来自 Beazley 的 Python Essential Reference, Fourth Edition,p。 194:

一些库模块提供了新的类型 优于内置插件 某些任务。例如, collections.deque 类型提供 与列表类似的功能,但 已经高度优化 在两端插入项目。一种 相比之下,列表仅是有效的 在末尾附加项目时。如果 你在前面插入项目,所有 其他元素需要移动 为了腾出空间。时间 随着列表的增长,需要这样做 变得越来越大。只为给 你知道区别,这里是在列表和双端队列前面插入一百万个项目的时间测量:

下面是这个代码示例:

>>> from timeit import timeit
>>> timeit('s.appendleft(37)', 'import collections; s = collections.deque()', number=1000000)
0.13162776274638258
>>> timeit('s.insert(0,37)', 's = []', number=1000000)
932.07849908298408

时间来自我的机器。


2012-07-01 更新

>>> from timeit import timeit
>>> n = 1024 * 1024
>>> while n > 1:
...     print '-' * 30, n
...     timeit('s.appendleft(37)', 'import collections; s = collections.deque()', number=n)
...     timeit('s.insert(0,37)', 's = []', number=n)
...     n >>= 1
... 
------------------------------ 1048576
0.1239769458770752
799.2552740573883
------------------------------ 524288
0.06924104690551758
148.9747350215912
------------------------------ 262144
0.029170989990234375
35.077512979507446
------------------------------ 131072
0.013737916946411133
9.134140014648438
------------------------------ 65536
0.006711006164550781
1.8818109035491943
------------------------------ 32768
0.00327301025390625
0.48307204246520996
------------------------------ 16384
0.0016388893127441406
0.11021995544433594
------------------------------ 8192
0.0008249282836914062
0.028419017791748047
------------------------------ 4096
0.00044918060302734375
0.00740504264831543
------------------------------ 2048
0.00021195411682128906
0.0021741390228271484
------------------------------ 1024
0.00011205673217773438
0.0006101131439208984
------------------------------ 512
6.198883056640625e-05
0.00021386146545410156
------------------------------ 256
2.9087066650390625e-05
8.797645568847656e-05
------------------------------ 128
1.5974044799804688e-05
3.600120544433594e-05
------------------------------ 64
8.821487426757812e-06
1.9073486328125e-05
------------------------------ 32
5.0067901611328125e-06
1.0013580322265625e-05
------------------------------ 16
3.0994415283203125e-06
5.9604644775390625e-06
------------------------------ 8
3.0994415283203125e-06
5.0067901611328125e-06
------------------------------ 4
3.0994415283203125e-06
4.0531158447265625e-06
------------------------------ 2
2.1457672119140625e-06
2.86102294921875e-06

【讨论】:

【参考方案3】:

一些答案​​声称双端队列与列表使用作为 FIFO 的速度优势为“10 倍”,但两者都有 1000 个条目,但这有点过高:

$ python -mtimeit -s'q=range(1000)' 'q.append(23); q.pop(0)'
1000000 loops, best of 3: 1.24 usec per loop
$ python -mtimeit -s'import collections; q=collections.deque(range(1000))' 'q.append(23); q.popleft()'
1000000 loops, best of 3: 0.573 usec per loop

python -mtimeit 是你的朋友——一种非常有用且简单的微基准测试方法!有了它,您当然也可以在更小的情况下轻松探索性能:

$ python -mtimeit -s'q=range(100)' 'q.append(23); q.pop(0)'
1000000 loops, best of 3: 0.972 usec per loop
$ python -mtimeit -s'import collections; q=collections.deque(range(100))' 'q.append(23); q.popleft()'
1000000 loops, best of 3: 0.576 usec per loop

(顺便说一句,12 个而不是 100 个项目差别不大),以及更大的项目:

$ python -mtimeit -s'q=range(10000)' 'q.append(23); q.pop(0)'
100000 loops, best of 3: 5.81 usec per loop
$ python -mtimeit -s'import collections; q=collections.deque(range(10000))' 'q.append(23); q.popleft()'
1000000 loops, best of 3: 0.574 usec per loop

您可以看到,关于双端队列的 O(1) 性能的说法是有根据的,而列表在 1,000 项左右的速度是两倍多,大约 10,000 个数量级。您还可以看到,即使在这种情况下,每个附加/弹出对仅浪费 5 微秒左右,并确定这种浪费的严重程度(尽管如果这就是您对该容器所做的全部,那么 deque 没有缺点,所以您即使 5 微秒或多或少不会产生重要影响,也可以切换)。

【讨论】:

谢谢,有用的测试。【参考方案4】:

使用list 实现不会耗尽内存,但性能会很差。来自the docs:

虽然list 对象支持类似的 操作,它们针对 快速的固定长度操作并招致 O(n) 内存移动成本 pop(0)insert(0, v) 操作 它改变了大小和 基础数据的位置 表示。

所以使用deque 会快很多。

【讨论】:

“快得多”?或者可能更快? 对于大小为 1000、10x 的列表。在我的书中,超过一个数量级“快得多”。 Lott:从列表中弹出是 O(N),从双端队列中弹出是 O(1)。 @John,您大错特错:CPython 使用引用计数和标记和清除,分代垃圾收集(您可以通过标准库中的 gc 模块在一定程度上控制) .所以“CPython 不使用垃圾收集”确实是一个严重缺陷的说法。 您实际上可能会使用列表耗尽内存。双端队列分配在不必彼此连续的桶中,因此基本上您可以创建一个与可用内存一样大的双端队列。然而,列表是数组,必须连续分配,如果它们的大小达到兆字节,您可能会发现这会给您带来一些麻烦(并且它们至少可能由于重新分配而导致严重的内存碎片)。跨度> 【参考方案5】:

听起来,在这里做一些经验测试可能是最好的选择——二阶问题可能会使一种方法在实践中变得更好,即使它在理论上并不更好。

【讨论】:

以上是关于使用 Python 列表作为队列的效率的主要内容,如果未能解决你的问题,请参考以下文章

Python 栈队列的实现

python-把列表当堆栈,队列使用

Python将列表作为栈和队列

作为客户端,如何获取 Stomp 队列或/和主题(它们的名称)列表?

使用字典 get() 作为函数从地图创建 Python 列表与使用 for 循环创建字典 get() 列表

python3的deque