【问题标题】:python: deque vs list performance comparisonpython:双端队列与列表性能比较
【发布时间】:2014-06-22 15:07:54
【问题描述】:

在 python 文档中,我可以看到 deque 是一个为从左侧或右侧弹出/添加项目高度优化的特殊集合。例如。文档说:

双端队列是堆栈和队列的概括(名称是 发音为“deck”,是“double-ended queue”的缩写)。双端队列 支持线程安全、内存高效的追加和弹出 在双端队列的一侧具有大致相同的 O(1) 性能 任一方向。

虽然列表对象支持类似的操作,但它们已针对 快速固定长度操作并产生 O(n) 内存移动成本 pop(0) 和 insert(0, v) 操作同时改变大小和 底层数据表示的位置。

我决定使用 ipython 进行一些比较。谁能解释我在这里做错了什么:

In [31]: %timeit range(1, 10000).pop(0)
 10000 loops, best of 3: 114 us per loop

In [32]: %timeit deque(xrange(1, 10000)).pop() 
10000 loops, best of 3: 181 us per loop

In [33]: %timeit deque(range(1, 10000)).pop()
1000 loops, best of 3: 243 us per loop

【问题讨论】:

  • 从列表(例如rangexrange)创建deque 对象需要O(n) 时间。
  • “错误”是什么意思?你预计会发生什么?
  • 同意@JayanthKoushik,在创建列表和双端队列之后时间.pop
  • deque 有内部锁来实现线程安全,但 list 没有。
  • @XingFei 不,collections.deque 没有内部锁。为此,您需要 Queue.Queue。但是 deque 的 append()、appendleft()、pop()、popleft() 和 len() 方法可以被认为是原子的,而不是通过契约的保证,而是通过它们在 CPython 中的实现方式。请参阅bugs.python.org/issue15329#msg199368(例如,对双端队列进行迭代不是线程安全的)。

标签: python performance data-structures benchmarking deque


【解决方案1】:

Could anyone explain me what I did wrong here

是的,您的时间主要取决于创建列表或双端队列的时间。相比之下,做 pop 的时间是微不足道的。

相反,您应该将您要测试的东西(弹出速度)与设置时间隔离开来:

In [1]: from collections import deque

In [2]: s = list(range(1000))

In [3]: d = deque(s)

In [4]: s_append, s_pop = s.append, s.pop

In [5]: d_append, d_pop = d.append, d.pop

In [6]: %timeit s_pop(); s_append(None)
10000000 loops, best of 3: 115 ns per loop

In [7]: %timeit d_pop(); d_append(None)
10000000 loops, best of 3: 70.5 ns per loop

也就是说,deques 和 list 在性能方面的真正区别是:

  • 对于 appendleft()popleft(),Deques 的速度为 O(1),而对于 insert(0),列表的速度为 O(n) , value)pop(0).

  • 列表追加性能受到打击,因为它在后台使用 realloc()。结果,它在简单代码中往往具有过度乐观的时序(因为 realloc 不必移动数据),而在实际代码中的时序非常慢(因为碎片迫使 realloc 移动所有数据)。相比之下,双端队列追加性能是一致的,因为它从不重新分配,也从不移动数据。

【讨论】:

  • 是的,它使用链表逻辑。更具体地说,它使用固定长度块的双向链表。
  • 对列表使用realloc() 只是一种优化。每次调整列表大小时都会过度分配列表以保证 O(1) 摊销附加性能,即使每次调整大小时都必须复制数据。
  • @augurar realloc() 的使用不是优化,它是列表增长的核心。过度分配策略是优化——该策略减少了对 realloc() 的调用次数,但并没有消除它们。 realloc() 仍会定期调用。这会降低时序的可重复性并且难以解释,因为 realloc 性能会因数据是否必须被复制而有很大差异。
  • @augurar 你完全没有抓住重点。是的,有摊销的 O(1) 性能。然而,常数因子变化很大,因为底层操作有时便宜有时昂贵。
  • @zyxue 我会更新答案。对于 Python 2,range() 返回一个列表是正确的。
【解决方案2】:

物有所值:

python3

deque.poplist.pop

>  python3 -mtimeit -s 'import collections' -s 'items = range(10000000); base = [*items]' -s 'c = collections.deque(base)' 'c.pop()'
5000000 loops, best of 5: 46.5 nsec per loop 
    
> python3 -mtimeit -s 'import collections' -s 'items = range(10000000); base = [*items]' 'base.pop()'
5000000 loops, best of 5: 55.1 nsec per loop

deque.appendleftlist.append

> python3 -mtimeit -s 'import collections' -s 'c = collections.deque()' 'c.appendleft(1)'
5000000 loops, best of 5: 52.1 nsec per loop

> python3 -mtimeit -s 'c = []' 'c.insert(0, 1)'
50000 loops, best of 5: 12.1 usec per loop

python2

> python -mtimeit -s 'import collections' -s 'c = collections.deque(xrange(1, 100000000))' 'c.pop()'
10000000 loops, best of 3: 0.11 usec per loop

> python -mtimeit -s 'c = range(1, 100000000)' 'c.pop()'
10000000 loops, best of 3: 0.174 usec per loop

> python -mtimeit -s 'import collections' -s 'c = collections.deque()' 'c.appendleft(1)'
10000000 loops, best of 3: 0.116 usec per loop

> python -mtimeit -s 'c = []' 'c.insert(0, 1)'
100000 loops, best of 3: 36.4 usec per loop

如您所见,它真正的亮点在于 appendleftinsert

【讨论】:

    【解决方案3】:

    我找到了解决这个问题的方法,并想提供一个带有一点背景的例子。
    使用 Deque 的经典用例可能是在集合中旋转/移动元素,因为(正如其他人所提到的),两端的 push/pop 操作的复杂性非常好(O(1)),因为这些操作只是移动引用而不是列表,它必须在内存中物理移动对象。

    所以这里有 2 个非常相似的左旋转函数实现:

    def rotate_with_list(items, n):
        l = list(items)
        for _ in range(n):
            l.append(l.pop(0))
        return l
    
    from collections import deque
    def rotate_with_deque(items, n):
        d = deque(items)
        for _ in range(n):
            d.append(d.popleft())
        return d
    

    注意:这是 deque 的常见用法,deque 有一个内置的 rotate 方法,但为了视觉比较,我在这里手动进行。

    现在让我们%timeit

    In [1]: def rotate_with_list(items, n):
       ...:     l = list(items)
       ...:     for _ in range(n):
       ...:         l.append(l.pop(0))
       ...:     return l
       ...: 
       ...: from collections import deque
       ...: def rotate_with_deque(items, n):
       ...:     d = deque(items)
       ...:     for _ in range(n):
       ...:         d.append(d.popleft())
       ...:     return d
       ...: 
    
    In [2]: items = range(100000)
    
    In [3]: %timeit rotate_with_list(items, 800)
    100 loops, best of 3: 17.8 ms per loop
    
    In [4]: %timeit rotate_with_deque(items, 800)
    The slowest run took 5.89 times longer than the fastest. This could mean that an intermediate result is being cached.
    1000 loops, best of 3: 527 µs per loop
    
    In [5]: %timeit rotate_with_list(items, 8000)
    10 loops, best of 3: 174 ms per loop
    
    In [6]: %timeit rotate_with_deque(items, 8000)
    The slowest run took 8.99 times longer than the fastest. This could mean that an intermediate result is being cached.
    1000 loops, best of 3: 1.1 ms per loop
    
    In [7]: more_items = range(10000000)
    
    In [8]: %timeit rotate_with_list(more_items, 800)
    1 loop, best of 3: 4.59 s per loop
    
    In [9]: %timeit rotate_with_deque(more_items, 800)
    10 loops, best of 3: 109 ms per loop
    

    非常有趣的是,这两种数据结构如何公开一个极其相似的接口,但性能却截然不同:)

    【讨论】:

      【解决方案4】:

      我建议你参考 https://wiki.python.org/moin/TimeComplexity

      Python 列表和双端队列对于大多数操作(推送、弹出等)具有相似的复杂性

      【讨论】:

      • 但重要的是,not for popleft / .pop(0) / pop 中间,这是这个问题 试图 衡量的。
      猜你喜欢
      • 2017-05-06
      • 2020-04-19
      • 2015-05-01
      • 2020-05-24
      • 2013-05-21
      • 2018-07-02
      • 2012-07-24
      • 2023-04-02
      • 2018-09-08
      相关资源
      最近更新 更多