【问题标题】:Why is slice assignment faster than `list.insert`?为什么切片分配比`list.insert`更快?
【发布时间】:2012-09-14 07:12:39
【问题描述】:

灵感来自this nice answer

这是一个基准:

import timeit

def test1():
    a = [1,2,3]
    a.insert(0,1)

def test2():
    a = [1,2,3]
    a[0:0]=[1]

print (timeit.timeit('test1()','from __main__ import test1'))
print (timeit.timeit('test2()','from __main__ import test2'))

对我来说,test2 稍微快一些(~10%)。为什么会这样?我希望它会更慢,因为:

  1. 切片分配必须能够接受任何长度的迭代,因此必须更通用。
  2. 在切片分配中,我们需要在右侧创建一个新列表才能使其正常工作。

谁能帮我理解这个?

(在 OS-X 10.5.8 上使用 python 2.7)

【问题讨论】:

  • 好问题 :) 我自己对此很好奇。
  • @TimPietzcker -- 你的基准测试真的让我陷入了困境。我必须自己测试它:)。
  • 我猜insert是通过调用切片分配代码来实现的。
  • @KeithRandall:不,两个代码路径是分开的(list_ass_slicehg.python.org/cpython/file/bfdf366a779a/Objects/listobject.c 中的ins1)。
  • @JoranBeasley -- 在我的基准测试中,列表没有增长。但是在 Tim Pietzcker 的(见链接的答案)中,它在基准测试的同时增长。 (我认为最长可达 100000 左右)

标签: python performance optimization python-internals


【解决方案1】:

您的第一个测试用例必须调用列表a 上的方法insert,而test2 中的所有操作都直接在字节码中处理。注意下面test1反汇编中的CALL_FUNCTION。在 Python 中调用函数的开销适中:当然开销足以解释运行时间的几个百分点差异。

>>> import dis
>>> dis.dis(test1)
  2           0 LOAD_CONST               1 (1)
              3 LOAD_CONST               2 (2)
              6 LOAD_CONST               3 (3)
              9 BUILD_LIST               3
             12 STORE_FAST               0 (a)

  3          15 LOAD_FAST                0 (a)
             18 LOAD_ATTR                0 (insert)
             21 LOAD_CONST               4 (0)
             24 LOAD_CONST               1 (1)
             27 CALL_FUNCTION            2
             30 POP_TOP             
             31 LOAD_CONST               0 (None)
             34 RETURN_VALUE        
>>> dis.dis(test2)
  2           0 LOAD_CONST               1 (1)
              3 LOAD_CONST               2 (2)
              6 LOAD_CONST               3 (3)
              9 BUILD_LIST               3
             12 STORE_FAST               0 (a)

  3          15 LOAD_CONST               1 (1)
             18 BUILD_LIST               1
             21 LOAD_FAST                0 (a)
             24 LOAD_CONST               4 (0)
             27 LOAD_CONST               4 (0)
             30 STORE_SLICE+3       
             31 LOAD_CONST               0 (None)
             34 RETURN_VALUE        

不好的解释

我先发布了这个,但经过考虑我认为它是不正确的。我在这里描述的差异只有在需要移动大量数据时才会产生明显的差异,而这里的测试并非如此。即使有大量数据,差异也只有百分之几:

import timeit

def test1():
    a = range(10000000)
    a.insert(1,1)

def test2():
    a = range(10000000)
    a[1:1]=[1]

>>> timeit.timeit(test1, number=10)
6.008707046508789
>>> timeit.timeit(test2, number=10)
5.861173868179321

方法list.insertlistobject.c中的函数ins1实现。您会看到它一一复制列表尾部的项目引用:

for (i = n; --i >= where; )
    items[i+1] = items[i];

另一方面,切片分配是由函数list_ass_slice实现的,它调用memmove

memmove(&item[ihigh+d], &item[ihigh],
        (k - ihigh)*sizeof(PyObject *));

所以我认为你的问题的答案是 C 库函数 memmove 比简单循环优化得更好。见here for the glibc implementation of memmove:我相信当从list_ass_slice 调用时,它最终会调用_wordcopy_bwd_aligned,你可以看到它是经过大量手工优化的。

【讨论】:

  • 我的预感是 test1 速度较慢,因为每个 a.insert 的属性查找和绑定方法创建。为了测试这一点,我将test1 定义为def test1(ins=list.insert),并将a.insert(0, 1) 替换为ins(a, 0, 1)——几乎没有可测量的差异。 :)
  • 我不相信你的新添加...a[0:0] 不需要隐式调用__setitem__(通过STORE_SLICE),因为python 无法知道a是列表而不是其他类型? (如果我错了,请纠正我......我不是阅读dis.dis 输出的专家)。调用__setitem__ 与调用insert 有什么不同?如果有的话,setitem 需要构造一个额外的 slice 对象,然后被解释...
  • @user4815162342 -- 是否也需要隐式地为a[0:0] 进行属性查找? (它如何访问__setitem__?)
  • 它没有——访问__setitem__(和许多其他特殊方法)是实现类型对象的C结构的cached in a member。它通过一个简单的指针取消引用来访问。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-11-10
  • 2018-06-06
  • 2019-04-26
  • 2015-10-15
  • 1970-01-01
  • 2011-05-01
相关资源
最近更新 更多