通过预分配和'my_list[x]=y'填充python列表,作为`\uu setitem\uu`的语法糖。

通过预分配和'my_list[x]=y'填充python列表,作为`\uu setitem\uu`的语法糖。,python,optimization,Python,Optimization,在为Python 2.7中的代码进行一些优化时,我偶然发现了以下现象: >>> from timeit import timeit >>> def fill_by_appending(): ... my_list = [] ... for x in xrange(1000000): ... my_list.append(x) ... return my_list ... >>> def fill

在为Python 2.7中的代码进行一些优化时,我偶然发现了以下现象:

>>> from timeit import timeit
>>> def fill_by_appending():
...     my_list = []
...     for x in xrange(1000000):
...             my_list.append(x)
...     return my_list
...
>>> def fill_preallocated_1():
...     my_list = [0]*1000000
...     for x in xrange(1000000):
...             my_list[x] = x
...     return my_list
...
>>> def fill_preallocated_2():
...     my_list = [0]*1000000
...     for x in xrange(1000000):
...             my_list.__setitem__(x,x)
...     return my_list
...
>>> def fill_by_comprehension():
...     my_list = [x for x in xrange(1000000)]
...     return my_list
...
>>> assert fill_by_appending() == fill_preallocated_1() == fill_preallocated_2() == fill_by_comprehension()
>>> timeit("fill_by_appending()", setup="from __main__ import fill_by_appending", number=100)
5.877948999404907
>>> timeit("fill_preallocated_1()", setup="from __main__ import fill_preallocated_1", number=100)
3.964423894882202
>>> timeit("fill_preallocated_2()", setup="from __main__ import fill_preallocated_2", number=100)
12.38241720199585
>>> timeit("fill_by_comprehension()", setup="from __main__ import fill_by_comprehension", number=100)
2.742932081222534
对我来说,预分配比追加快,或者理解比其他任何东西都快,这并不奇怪,但是为什么使用
\uuuu setitem\uuuu
比使用
[]
慢三倍呢

最初,我有一个理论,即使用
my_list[x]=x
仅仅是将存储在
my_list[x]
中的引用重新分配给新对象的地址,或者解释器甚至注意到这两个对象的类型相同,并且使用了重载赋值运算符,然而,
setitem
调用实际上复制了内存,但一些实验证明我错了:

>>> class MyList(list):
...     def __setitem__(self, x,y):
...             super(MyList, self).__setitem__(x,y**2)
...
>>> ml = MyList([1,2,3,4,5])
>>> ml[2]=10
>>> ml
[1, 2, 100, 4, 5]

有人知道引擎盖下面发生了什么吗

在第二个函数中有额外的属性查找+函数调用:

def fill\u preallocated\u 1():

def fill\u preallocated\u 2():


泛型方法分派比基于语法的分派有额外的开销;后者,而前者必须重复查找并创建绑定方法,并通过泛型方法分派机制分派调用(更一般==更慢)。泛型分派还意味着构造要传递的参数的
元组
(其中基于语法的调用只读取Python堆栈中的值,而不构造
元组

此外,Python级别的名称实际上是一个薄包装,因此调用
\uuuu setitem\uuuuu
意味着在到达C API之前需要进行一个额外的调用层,因为它必须在到达
sq\u ass\u item
之前遍历一个额外的层(C层插槽是实现分配的最终调用)。根据文件,但看起来像

您可以通过存储非绑定方法来消除方法查找和绑定开销,这可能会节省一点工作,但从根本上说,对于CPython,语法优于方法调用;如果语法同样清晰且不容易出错,请使用语法。可以减少部分差异的预绑定示例如下:

def fill_preallocated_3():
    my_list = [0]*1000000
    set = my_list.__setitem__
    for x in xrange(1000000):
        set(x,x)
    return my_list

[x]=
对解释器有特殊的意义时,我猜
\uuuuu setitem\uuuuuuuuuu
是一个函数调用。创建新列表时,
[]
list()
快的原因也是一样的。旁注:对于这种特殊情况,真正的答案是“只需调用
range(1000000)
”(或者在Py3上,
list(range(1000000))
,如果您真的需要
list
),但是我假设您正在考虑稍微复杂一点的填充模式。@ShadowRanger稍微:)注意:属性查找成本可能只是通用
CALL\u函数
与特定
STORE\u子函数
开销的一小部分。包括一个变体测试,该测试将消除属性查找费用,以帮助隔离减速的组件。函数调用是主要原因-缓存
\uuuuuu setitem\uuuuuuuu
属性可使我的系统提高30%左右。不是要扔掉的东西,但优化的列表分配速度快了三倍多。顺便说一句,在我的系统上,相同的测试:
fill\u preallocated\u 1:4.73s;填充预分配的2:13.41s;填充预分配的\u 3:10.8s
谢谢确认!谢谢你,特别是CPython源代码的链接!你知道为什么选择将
\uuuuu setitem\uuuuu
放在额外的层后面吗?是否与继承有关?@gmoss:No。他们为每个定义的C级插槽都使用了一个通用包装器;C级插槽接受已经解析的参数,这样C->C调用就不需要打包只能立即解包的参数;包装器提供了从Python级别的通用调用约定到特定于插槽的调用原型的转换层。这是通用“varargs-like”函数分派与特定C级函数原型的成本的一部分。作为记录,不总是使用
METH\u共存的原因是它通常不需要(
\uuuuu setitem\uuuuu
通常不直接调用;通常通过语法分派路径绕过它直接进入C级插槽),使其比解析并传递到C插槽的包装器有意义地快的唯一方法是在共存的Python公开函数中复制C级函数的大部分代码(基本上,复制粘贴大量代码)。不值得为一个相当小的收益而进行维护。
         ...
         32 LOAD_FAST                0 (my_list)
         35 LOAD_ATTR                1 (__setitem__)
         38 LOAD_FAST                1 (x)
         41 LOAD_FAST                1 (x)
         44 CALL_FUNCTION            2
         ...
def fill_preallocated_3():
    my_list = [0]*1000000
    set = my_list.__setitem__
    for x in xrange(1000000):
        set(x,x)
    return my_list