为什么对于 bytearray 来说 b.pop(0) 比 del b[0] 慢 200 多倍?

Kel*_*ndy 57 python arrays performance cpython python-internals

让他们竞争 3 次(每次 100 万次弹出/删除):

\n
from timeit import timeit\n\nfor _ in range(3):\n    t1 = timeit(\'b.pop(0)\', \'b = bytearray(1000000)\')\n    t2 = timeit(\'del b[0]\', \'b = bytearray(1000000)\')\n    print(t1 / t2)\n
Run Code Online (Sandbox Code Playgroud)\n

时间比例(在线尝试!):

\n
274.6037053753368\n219.38099365582403\n252.08691226683823\n
Run Code Online (Sandbox Code Playgroud)\n

为什么pop做同样的事情要慢得多?

\n

int*_*jay 82

当您运行 时b.pop(0),Python 将按照您的预期将所有元素向后移动 1。这需要 O(n) 时间。

当你这样做时del b[0],Python只是将对象的起始指针加1。

在这两种情况下,PyByteArray_Resize都会调用调整大小。当新大小小于已分配大小的一半时,分配的内存将收缩。在这种del b[0]情况下,这是复制数据的唯一点。因此,这种情况将花费 O(1) 摊余时间。

相关代码:

bytearray_pop_impl功能:总是调用

memmove(buf + index, buf + index + 1, n - index);
Run Code Online (Sandbox Code Playgroud)

bytearray_setslice_linear函数del b[0]通过lo == 0, hi == 1,来调用bytes_len == 0。它到达此代码(带有growth == -1):

if (lo == 0) {
    /* Shrink the buffer by advancing its logical start */
    self->ob_start -= growth;
    /*
      0   lo               hi             old_size
      |   |<----avail----->|<-----tail------>|
      |      |<-bytes_len->|<-----tail------>|
      0    new_lo         new_hi          new_size
    */
}
else {
    /*
      0   lo               hi               old_size
      |   |<----avail----->|<-----tomove------>|
      |   |<-bytes_len->|<-----tomove------>|
      0   lo         new_hi              new_size
    */
    memmove(buf + lo + bytes_len, buf + hi,
            Py_SIZE(self) - hi);
}
Run Code Online (Sandbox Code Playgroud)

  • @JérômeRichard 我已经看过很多次了。他们不想优化不常见的情况。两者都是为了避免稍微减慢“常见”情况(可能总体上是净损失!),并避免需要编写、测试、审查、维护、读取的额外代码,这些代码可能会引入错误,并且可能会使事情变得复杂。请参阅原始的[切片优化的讨论](https://github.com/python/cpython/issues/63287)以获得所有这一切的完美示例。 (24认同)
  • 好答案。您知道为什么开发人员不选择为“b.pop(0)”也增加指针吗? (16认同)
  • 有人有“切片”优化(FIFO 缓冲区)的用例,这有点像一场战斗。有趣的是,在某些时候他们谈到了“popping”,但不是在谈论 pop() 而是在谈论删除一个切片。他们从未谈论过弹出单个字节,而且我确实相信这对于字节数组来说确实不常见。甚至从最后开始。尤其是当阵列和我一样大的时候。而且当数组小很多的时候我的速度比也小很多。 (5认同)
  • @PM2Ring 嗯,我不这么认为。我们“知道”为什么要实施“切片”删除,这在链接的原始讨论中。我*怀疑*(没有检查)具有单个索引的“del”作为副作用得到了优化。优化“pop”甚至从未被提及,至少在那次讨论中是这样。 (4认同)
  • 所以我怀疑没有人*希望* bytearray 的 `pop(0)` 被优化。我也没有,我对它没有真正的用处,我只是在寻找其他东西时偶然发现了它。如果我“确实”需要使用它,我很可能会使用“del”来代替,并且会这样做。 (2认同)
  • @PM2Ring [差异](https://github.com/python/cpython/commit/5df8a8a1fd6cc6f4469dc7d3994d06e2aea24c52),以防有人想检查是否明确执行了任何操作来优化简单的索引删除。 (2认同)

Dil*_*vis 27

我必须承认,我自己对这个时间安排感到非常惊讶。在说服自己它们实际上是正确的之后,我深入研究了 CPython 源代码,我想我找到了答案 - cpythondel bytearr[0:x]通过仅增加指向数组开头的指针来优化:

    if (growth < 0) {
        if (!_canresize(self))
            return -1;

        if (lo == 0) {
            /* Shrink the buffer by advancing its logical start */
            self->ob_start -= growth;
Run Code Online (Sandbox Code Playgroud)

您可以在此处找到del bytearray[...]逻辑(通过和being实现),该逻辑又调用,其中包含上述优化。bytearray_setslicevaluesNULLbytearray_setslice_linear

为了进行比较,bytearray.pop 没有实现此优化 - 请参阅此处的源代码。