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

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

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

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

【问题讨论】:

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

标签: python list memory-leaks


【解决方案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】:

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

      【讨论】: