【问题标题】:Why is deque implemented as a linked list instead of a circular array?为什么 deque 实现为链表而不是循环数组?
【发布时间】:2017-12-21 09:28:37
【问题描述】:

CPython dequeimplemented 作为 64 项大小的“块”(数组)的双向链表。块都是满的,除了链表两端的块。 IIUC,当 pop / popleft 删除块中的最后一项时,块被释放;它们在append/appendleft 尝试添加新项目并且相关块已满时分配。

我理解the listed advantages 使用块的链接列表而不是项目的链接列表:

  • 减少每个项目中指向 prev 和 next 的指针的内存成本
  • 为添加/删除的每个项目减少 malloc/free 的运行时成本
  • 通过将连续指针彼此相邻放置来提高缓存局部性

但为什么一开始不使用单个动态大小的循环数组来代替双向链表呢?

AFAICT,循环数组将保留上述所有优势,并将pop*/append* 的(摊销)成本维持在O(1)(通过过度分配,就像在list 中一样)。此外,它将按索引查找的成本从当前的O(n) 提高到O(1)。循环数组也更容易实现,因为它可以重用大部分 list 实现。

我可以在 C++ 等语言中看到支持链表的论点,其中可以在 O(1) 中使用指针或迭代器从中间删除项目;但是,python deque 没有 API 可以做到这一点。

【问题讨论】:

  • 链表没有引人注目的优势。除非有人能挖掘出相关的邮件列表讨论或其他内容,否则我们所能做的就是将其归结为 collections.deque 推出时某个人的心血来潮。
  • 根据这个问题的结果,也许可以实现您的想法,针对 CPython 测试运行它,进行一些基准测试,然后提交拉取请求。
  • @user2357112 我会同意你的看法,但有问题的人是超级熟练的,并且在没有仔细分析的情况下从不做任何事情:)
  • 挖掘Raymond Hettinger's collections module proposal to Python-Dev,这可能是关于collections.deque 的第一个公开讨论之一,没有给出链表结构的理由,我也没有看到任何讨论它的回复。最接近基本原理的是“就像我们用于 itertools.tee() 一样”,这表明他们可能想重用现有代码(对双链接进行修改)。
  • This 来自 RDH 的消息似乎是您问题的答案,不像您可能想要的那样冗长。 previous message 讨论了替代实现。

标签: python python-3.x cpython python-internals


【解决方案1】:

改编自我在 python-dev 邮件列表上的回复:

双端队列的主要目的是使两端的弹出和推送高效。这就是当前实现所做的:无论双端队列中有多少项,每次推送或弹出的最坏情况恒定时间。这在小型和大型方面都超过了“摊销 O(1)”。这就是为什么这样做的原因。

因此,其他一些双端队列方法比列表慢,但谁在乎呢?例如,我曾经在双端队列中使用过的唯一索引是 0 和 -1(用于查看双端队列的一端或另一端),并且该实现也使访问这些特定索引的时间保持不变。

事实上,Jim Fasarakis Hilliard 在他的评论中引用了 Raymond Hettinger 的信息:

https://www.mail-archive.com/python-dev@python.org/msg25024.html

确认

放入__getitem__的唯一原因是支持快速访问头部和尾部而不实际弹出值

【讨论】:

    【解决方案2】:

    除了接受@TimPeters 的回答之外,我还想添加一些不适合评论格式的额外观察。

    让我们只关注一个常见的用例,其中deque 用作简单的 FIFO 队列。

    一旦队列达到峰值大小,循环数组就不再需要从堆中分配内存。我认为这是一个优势,但事实证明 CPython 实现通过保留可重用内存块的列表来实现相同的目标。一条领带。

    随着队列大小的增加,循环数组和 CPython 都需要堆内存。 CPython 需要一个简单的malloc,而数组需要(可能更昂贵)realloc(除非在原始内存块的右边缘有额外的空间可用,它需要释放旧内存并复制数据结束)。 CPython 的优势。

    如果队列达到峰值时比其稳定大小大得多,CPython 和数组实现都会浪费未使用的内存(前者将其保存在可重用的块列表中,后者将未使用的空白空间留在数组)。一条领带。

    正如@TimPeters 所指出的,对于 CPython,每个 FIFO 队列 put / get 的成本始终为 O(1),但对于数组仅摊销 O(1)。 CPython 的优势。

    【讨论】:

    • 请注意,在最后一种情况下(双端队列的峰值比其稳定大小大得多),双端队列实现“浪费”的空间受一个小常数的限制:内部块空闲列表最多包含MAXFREEBLOCKS双端队列块,即16个。如果列表中已经有那么多块,则随后释放的块通过PyMem_Free()返回给“系统”。块空闲列表实际上是为了提高效率。 FIFO 情况下,对象以大致相同的速率被推送和弹出。
    • @TimPeters 不错,CPython 实现的另一个优势。专注于一个重要用例的好处的一个很好的例子。
    • @max 感谢您提出这个很好的问题和答案。但我仍然很困惑。这里的讨论认为,双端队列的双向链表实现总是 O(1),而循环数组摊销 O(1),这是链表实现的一个好处。但是指针引用不是比数组索引慢得多吗?即使调整了数组大小,也会一次分配双数组空间。但是在链表的情况下,每次插入新项目时都会分配一个新的内存单元(通过malloc)(这不是更慢吗?)。
    • 另外,看看 Java 的 Deque 接口的实现。 Java 提供了这两种方法:即 LinkedList,一种 Deque 的双向链表实现,和 ArrayDeque,一种 Deque 的循环数组实现。 Java 文档中明确指出“这个类 (ArrayDeque) 在用作堆栈时可能比 Stack 快,而在用作队列时比 LinkedList 快”。见:docs.oracle.com/en/java/javase/15/docs/api/java.base/java/util/…
    猜你喜欢
    • 2017-10-27
    • 1970-01-01
    • 2021-05-16
    • 2012-09-27
    • 2017-07-10
    • 2012-08-16
    • 1970-01-01
    • 2018-10-23
    • 1970-01-01
    相关资源
    最近更新 更多