为什么[]比list()更快?

Aug*_*sta 679 python performance list instantiation literals

我最近比较了处理速度[]和,list()并且惊讶地发现它的[]运行速度比它三倍list().我跑了相同的测试与{}dict(),结果几乎相同:[]{}两个花了大约0.128sec /百万次,而list()dict()大约花费每个0.428sec /万次.

为什么是这样?不要[]{}(可能()'',太)立即传回文字的一些空股票的份,而其明确命名同行(list(),dict(),tuple(),str())完全去创建一个对象,他们是否真的有元素?

我不知道这两种方法有何不同,但我很想知道.我无法在文档中或在SO上找到答案,搜索空括号结果比我预期的问题更多.

我拨打了我的时序结果timeit.timeit("[]")timeit.timeit("list()"),和timeit.timeit("{}")timeit.timeit("dict()"),分别比较列表和字典.我正在运行Python 2.7.9.

我最近发现的" 为什么是,如果真慢于如果为1? "来比较的性能if Trueif 1,似乎触及了类似的文字,对全局的情况; 也许值得考虑一下.

Mar*_*ers 731

因为[]并且{}字面语法.Python可以创建字节码只是为了创建列表或字典对象:

>>> import dis
>>> dis.dis(compile('[]', '', 'eval'))
  1           0 BUILD_LIST               0
              3 RETURN_VALUE        
>>> dis.dis(compile('{}', '', 'eval'))
  1           0 BUILD_MAP                0
              3 RETURN_VALUE        
Run Code Online (Sandbox Code Playgroud)

list()并且dict()是单独的对象.需要解析它们的名称,必须涉及堆栈以推送参数,必须存储帧以便稍后检索,并且必须进行调用.这都需要更多时间.

对于空的情况,这意味着你至少有一个LOAD_NAME(必须搜索全局命名空间以及__builtin__模块)后面跟a CALL_FUNCTION,它必须保留当前帧:

>>> dis.dis(compile('list()', '', 'eval'))
  1           0 LOAD_NAME                0 (list)
              3 CALL_FUNCTION            0
              6 RETURN_VALUE        
>>> dis.dis(compile('dict()', '', 'eval'))
  1           0 LOAD_NAME                0 (dict)
              3 CALL_FUNCTION            0
              6 RETURN_VALUE        
Run Code Online (Sandbox Code Playgroud)

您可以使用以下命令单独查找名称timeit:

>>> import timeit
>>> timeit.timeit('list', number=10**7)
0.30749011039733887
>>> timeit.timeit('dict', number=10**7)
0.4215109348297119
Run Code Online (Sandbox Code Playgroud)

时间差异可能存在字典哈希冲突.从调用这些对象的时间减去这些时间,并将结果与​​使用文字的时间进行比较:

>>> timeit.timeit('[]', number=10**7)
0.30478692054748535
>>> timeit.timeit('{}', number=10**7)
0.31482696533203125
>>> timeit.timeit('list()', number=10**7)
0.9991960525512695
>>> timeit.timeit('dict()', number=10**7)
1.0200958251953125
Run Code Online (Sandbox Code Playgroud)

因此,必须调用该对象1.00 - 0.31 - 0.30 == 0.39每1000万次调用需要额外的秒数.

您可以通过将全局名称别名为locals来避免全局查找成本(使用timeit设置,绑定到名称的所有内容都是本地的):

>>> timeit.timeit('_list', '_list = list', number=10**7)
0.1866450309753418
>>> timeit.timeit('_dict', '_dict = dict', number=10**7)
0.19016098976135254
>>> timeit.timeit('_list()', '_list = list', number=10**7)
0.841480016708374
>>> timeit.timeit('_dict()', '_dict = dict', number=10**7)
0.7233691215515137
Run Code Online (Sandbox Code Playgroud)

但你永远无法克服CALL_FUNCTION成本.


Dan*_* D. 143

list()需要全局查找和函数调用,但[]编译为单个指令.看到:

Python 2.7.3
>>> import dis
>>> print dis.dis(lambda: list())
  1           0 LOAD_GLOBAL              0 (list)
              3 CALL_FUNCTION            0
              6 RETURN_VALUE        
None
>>> print dis.dis(lambda: [])
  1           0 BUILD_LIST               0
              3 RETURN_VALUE        
None
Run Code Online (Sandbox Code Playgroud)


Tor*_*xed 75

因为list是一个功能转化说一个字符串列表对象,而[]用于创建一个列表蝙蝠.试试这个(可能对你更有意义):

x = "wham bam"
a = list(x)
>>> a
["w", "h", "a", "m", ...]
Run Code Online (Sandbox Code Playgroud)

y = ["wham bam"]
>>> y
["wham bam"]
Run Code Online (Sandbox Code Playgroud)

为您提供包含您放入其中的任何内容的实际列表.

  • 这并没有直接解决这个问题.问题是为什么`[]`比`list()`更快,而不是为什么`['wham bam']`比`list('wham bam')`更快. (6认同)
  • @JeremyVisser 相反,它表明他们对内容进行了不同的操作。 (3认同)
  • @JeremyVisser这对我来说意义不大,因为`[] // list()`与`['wham']`/`list('wham')`完全相同,因为它们的变量差异与`数学上,“ 1000/10”与“ 100/1”相同。从理论上讲,您可以删除`wham bam`,事实仍然是一样的,`list()`试图通过调用函数名来转换某些内容,而`[]`会直接转换变量。函数调用是不同的,是的,这只是对该问题的逻辑概述,例如,公司的网络图也是解决方案/问题的逻辑。随心所欲投票。 (2认同)

Jim*_*ard 19

这里的答案很棒,很重要,完全涵盖了这个问题.对于那些感兴趣的人,我会从字节码中再下一步.我正在使用最新的CPython回购; 旧版本在这方面表现相似,但可能会有细微的变化.

这里有一个休息的执行对于这些,下来BUILD_LIST[],并CALL_FUNCTIONlist().


BUILD_LIST指令:

你应该只看恐怖:

PyObject *list =  PyList_New(oparg);
if (list == NULL)
    goto error;
while (--oparg >= 0) {
    PyObject *item = POP();
    PyList_SET_ITEM(list, oparg, item);
}
PUSH(list);
DISPATCH();
Run Code Online (Sandbox Code Playgroud)

我知道,非常复杂.这是多么简单:

  • 创建一个新列表PyList_New(这主要为新列表对象分配内存),oparg表示堆栈中的参数数量.开门见山.
  • 检查没有出错if (list==NULL).
  • 使用PyList_SET_ITEM(宏)添加位于堆栈上的任何参数(在我们的示例中,这不会被执行).

难怪它快!它是为创建新列表而定制的,没有别的:-)

CALL_FUNCTION指令:

这是您在查看代码处理时首先看到的内容CALL_FUNCTION:

PyObject **sp, *res;
sp = stack_pointer;
res = call_function(&sp, oparg, NULL);
stack_pointer = sp;
PUSH(res);
if (res == NULL) {
    goto error;
}
DISPATCH();
Run Code Online (Sandbox Code Playgroud)

看起来很无害,对吧?嗯,不,不幸的call_function是,不是一个直接调用函数的直接人,它不能.相反,它从堆栈中抓取对象,抓取堆栈的所有参数,然后根据对象的类型进行切换; 它是一个:

我们正在调用list类型,传入的参数call_functionPyList_Type.CPython现在必须调用泛型函数来处理任何名为的可调用对象_PyObject_FastCallKeywords,以及更多的函数调用.

这个函数再次对某些函数类型进行了一些检查(我无法理解为什么),然后,如果需要,在为kwargs创建一个dict之后,继续调用_PyObject_FastCallDict.

_PyObject_FastCallDict终于让我们到了某个地方!执行后甚至更多的检查抓住了tp_call从插槽中typetype,我们已经通过了,那就是它抓住type.tp_call.然后它继续从传入的参数中创建一个元组_PyStack_AsTuple,最后,最终可以进行调用!

tp_call,匹配type.__call__接管并最终创建列表对象.它调用与之__new__对应的列表PyType_GenericNew并为其分配内存PyType_GenericAlloc:这实际上是它赶上的部分PyList_New,最后.所有以前的都是以通用方式处理对象所必需的.

最后,type_call调用list.__init__并使用任何可用的参数初始化列表,然后我们继续返回我们的方式.:-)

最后,请记住LOAD_NAME,这是另一个在这里贡献的人.


很容易看出,在处理我们的输入时,Python通常必须跳过箍以实际找到适当的C函数来完成这项工作.它没有立即调用它的简单性,因为它是动态的,有人可能会掩盖list(男孩会做很多人这样做),必须采取另一条路径.

这就是list()失去的地方:探索Python需要做的是找出它应该做些什么.

另一方面,字面语法恰恰意味着一件事; 它不能改变,总是以预先确定的方式行事.

脚注:所有功能名称可能会从一个版本更改为另一个版本.这个观点仍然存在,而且很可能会出现在任何未来的版本中,它是动态的查找,可以减慢速度.


Aar*_*all 11

为什么[]比快list()

最大的原因是Python list()就像用户定义的函数一样,这意味着你可以通过别名来拦截它list并做一些不同的事情(比如使用你自己的子列表或者deque).

它立即创建一个内置列表的新实例[].

我的解释试图给你直觉.

说明

[] 通常称为文字语法.

在语法中,这被称为"列表显示".来自文档:

列表显示是一个可能为空的系列表达式,用方括号括起来:

list_display ::=  "[" [starred_list | comprehension] "]"
Run Code Online (Sandbox Code Playgroud)

列表显示产生一个新的列表对象,内容由表达式列表或理解指定.当提供以逗号分隔的表达式列表时,其元素将从左到右进行计算,并按该顺序放入列表对象中.当提供理解时,列表由理解产生的元素构成.

简而言之,这意味着list创建了一个内置的类型对象.

没有规避这个 - 这意味着Python可以尽可能快地完成它.

另一方面,list()可以list使用内置列表构造函数拦截创建内置函数.

例如,假设我们想要大声创建列表:

class List(list):
    def __init__(self, iterable=None):
        if iterable is None:
            super().__init__()
        else:
            super().__init__(iterable)
        print('List initialized.')
Run Code Online (Sandbox Code Playgroud)

然后,我们可以list在模块级全局范围内截取名称,然后在创建时list,我们实际创建了子类型列表:

>>> list = List
>>> a_list = list()
List initialized.
>>> type(a_list)
<class '__main__.List'>
Run Code Online (Sandbox Code Playgroud)

同样,我们可以从全局命名空间中删除它

del list
Run Code Online (Sandbox Code Playgroud)

并将其放在内置命名空间中:

import builtins
builtins.list = List
Run Code Online (Sandbox Code Playgroud)

现在:

>>> list_0 = list()
List initialized.
>>> type(list_0)
<class '__main__.List'>
Run Code Online (Sandbox Code Playgroud)

请注意,列表显示无条件地创建列表:

>>> list_1 = []
>>> type(list_1)
<class 'list'>
Run Code Online (Sandbox Code Playgroud)

我们可能只暂时执行此操作,因此请撤消我们的更改 - 首先List从内置中删除新对象:

>>> del builtins.list
>>> builtins.list
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: module 'builtins' has no attribute 'list'
>>> list()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'list' is not defined
Run Code Online (Sandbox Code Playgroud)

哦,不,我们失去了对原作的追踪.

不用担心,我们仍然可以得到list- 这是列表文字的类型:

>>> builtins.list = type([])
>>> list()
[]
Run Code Online (Sandbox Code Playgroud)

所以...

为什么[]比快list()

正如我们所见 - 我们可以覆盖list- 但我们不能拦截文字类型的创建.当我们使用时,list我们必须进行查找以查看是否有任何内容.

然后我们必须调用我们查找过的任何可调用对象.从语法:

调用使用可能为空的参数系列调用可调用对象(例如,函数):

call                 ::=  primary "(" [argument_list [","] | comprehension] ")"
Run Code Online (Sandbox Code Playgroud)

我们可以看到它对任何名称都做同样的事情,而不仅仅是列表:

>>> import dis
>>> dis.dis('list()')
  1           0 LOAD_NAME                0 (list)
              2 CALL_FUNCTION            0
              4 RETURN_VALUE
>>> dis.dis('doesnotexist()')
  1           0 LOAD_NAME                0 (doesnotexist)
              2 CALL_FUNCTION            0
              4 RETURN_VALUE
Run Code Online (Sandbox Code Playgroud)

因为[]在Python字节码级别没有函数调用:

>>> dis.dis('[]')
  1           0 BUILD_LIST               0
              2 RETURN_VALUE
Run Code Online (Sandbox Code Playgroud)

它只是直接构建列表而没有在字节码级别进行任何查找或调用.

结论

我们已经演示了list可以使用作用域规则拦截用户代码,并list()查找可调用然后调用它.

然后[]是列表显示或文字,从而避免名称查找和函数调用.

  • +1指出你可以劫持`list`而python编译器不能确定它是否真的会返回一个空列表. (2认同)