【问题标题】:Python list prepend time complexityPython列表前置时间复杂度
【发布时间】:2017-12-27 06:43:55
【问题描述】:

为什么是这个代码

res = []
for i in range(10000):
    res[0:0] = [i]

比这段代码快十倍?

res = []
for i in range(10000):
    res = [i] + res

我预计两者都必须移动所有现有列表元素以将新整数置于零索引处。随着范围的改变,两者确实看起来确实是 O(n^2),但是切片分配比相加要快得多,这意味着后者的基本操作大约是 10 倍。

(是的,两者都无法达到这个结果,最好使用dequeappend然后反转结果)

【问题讨论】:

  • FWIW,你也可以res.insert(0, i)。这应该几乎和切片分配一样快。

标签: python time-complexity


【解决方案1】:

您是对的,在较高的层次上,循环以相同的方式计算基本相同的结果。因此,时间差异是由于使用的 Python 版本的实现细节造成的。没有语言的属性可以解释这种差异。

在 python.org C 实现 (CPython) 中,代码实际上是完全不同的。

res[0:0] = [i]

做它看起来做的事情 ;-) res 的全部内容向右移动了一个槽,i 被插入到左端创建的孔中。对平台 C 库的 memmove() 函数的一次调用会消耗大量时间,该函数会大量转换。现代硬件和 C 库非常擅长快速移动连续的内存片(在 C 级别,Python 列表对象就是这样)。

res = [i] + res

在幕后做的更多,主要是由于 CPython 的引用计数。更像是:

create a brand new list object
stuff `i` into it
for each element of `res`, which is a pointer to an int object:
    copy the pointer into the new list object
    dereference the pointer to load the int object's refcount
    increment the refcount
    store the new refcount back into the int object
bind the name `res` to the new list object
decrement the refcount on the old `res` object
at which point the old res's refcount becomes 0 so it's trash
so for each object in the old res:
    dereference the pointer to load the int object's refcount
    decrement the refcount
    store the new refcount back into the int object
    check to see whether the new refcount is zero
    take the "no, it isn't zero" branch
release the memory for the old list object

更多的原始工作,所有指针解引用都可能跨越内存,这对缓存不友好。

实现

res[0:0] = [i]

跳过大部分内容:它从一开始就知道仅仅移动 res 的内容的位置不能对移动对象的引用计数进行任何净更改,因此不会费心增加或减少任何那些引用。 C 级别的memmove() 几乎是一团蜡,不需要取消引用任何指向 int 对象的指针。不仅原始工作更少,而且对缓存非常友好。

【讨论】:

  • 谢谢,在某种程度上很高兴知道这不是我误解的 Python 语言本身的属性,但它也表明从实现中可以学到很多东西。跨度>
  • 但不要过度概括 ;-) 您在这里偶然发现了一个极端情况。如果您想要一个“一般原则”,那么在效率很重要的情况下,通常情况下变异对象会比构建新对象运行得更快。即使没有此处的 refcount 扭曲,您也可以预期在每次迭代中构建和销毁列表的运行速度要比在整个过程中更改单个列表对象慢得多。
【解决方案2】:

在每个示例的相关行上运行反汇编,我们得到以下字节码:

res[0:0] = [i]

  4          25 LOAD_FAST                1 (i)
             28 BUILD_LIST               1
             31 LOAD_FAST                0 (res)
             34 LOAD_CONST               2 (0)
             37 LOAD_CONST               2 (0)
             40 BUILD_SLICE              2
             43 STORE_SUBSCR

res = [i] + res

  4          25 LOAD_FAST                1 (i)
             28 BUILD_LIST               1
             31 LOAD_FAST                0 (res)
             34 BINARY_ADD
             35 STORE_FAST               0 (res)

在第一个示例(切片)中,没有进行BINARY_ADD,只进行了存储操作,在加法的情况下,不仅有存储操作,还有BINARY_ADD 操作,它做得更多,这可能就是它慢得多的原因。虽然切片表示法确实需要构建切片,但这些操作也非常简单。

为了更公平的比较,如果切片符号是预先构造和存储的(使用类似s = slice(0, 0)),我们可以通过查找替换切片符号;生成的字节码如下所示:

res[s] = [i]

  4          25 LOAD_FAST                1 (i)
             28 BUILD_LIST               1
             31 LOAD_FAST                0 (res)
             34 LOAD_GLOBAL              1 (s)
             37 STORE_SUBSCR

这使得它具有相同数量的字节码指令计数,现在我们只看到加载和存储指令,而具有+ 操作的指令实际上需要额外的指令。

【讨论】:

    猜你喜欢
    • 2017-01-13
    • 2019-06-14
    • 1970-01-01
    • 1970-01-01
    • 2014-03-27
    • 2019-01-15
    • 2016-08-20
    • 1970-01-01
    • 2014-01-28
    相关资源
    最近更新 更多