为什么切片分配比`list.insert`更快?

时间:2012-09-21 20:26:10

标签: python performance optimization python-internals

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. 在切片分配中,我们需要在右侧创建一个新列表,以使其正常工作。
  3. 有人可以帮我理解吗?

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

1 个答案:

答案 0 :(得分:14)

您的第一个测试用例必须调用列表insert上的方法a,而test2中的所有操作都直接在字节码中处理。请注意下面CALL_FUNCTION反汇编中的test1。在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,您可以看到它是手工优化的。