为什么[]比list()快?

我最近比较了[]list()的处理速度,惊讶地发现[]list()运行list()0。我对{}dict()进行了相同的测试,结果几乎相同:[]{}都花费了大约0.128秒/百万次循环,而list()dict()分别花费了大约0.428秒/百万次循环。

为什么呢?[]{}(可能还有()'')会立即传回一些空的库存文字的副本,而它们显式命名的对应物(list()dict()tuple()str())会完全创建一个对象,不管它们是否真的有元素?

我不知道这两种方法有什么不同,但我很想知道。我在文档或SO中找不到答案,搜索空括号比我预期的要困难得多。

我通过调用timeit.timeit("[]")timeit.timeit("list()")以及timeit.timeit("{}")timeit.timeit("dict()")分别比较列表和字典来获得计时结果。我正在运行Python 2.7.9。

我最近发现了“为什么if true比if 1慢?”,它比较了if Trueif 1的性能,似乎触及了类似的文字与全局场景;也许它也值得考虑。

86398 次浏览

因为list是将say字符串转换为列表对象的函数,而[]用于立即创建列表。试试这个(可能对你更有意义):

x = "wham bam"a = list(x)>>> a["w", "h", "a", "m", ...]

虽然

y = ["wham bam"]>>> y["wham bam"]

给你一个实际的列表,包含你放进去的任何东西。

因为[]{}文字语法。Python可以创建字节码来创建列表或字典对象:

>>> import dis>>> dis.dis(compile('[]', '', 'eval'))1           0 BUILD_LIST               03 RETURN_VALUE>>> dis.dis(compile('{}', '', 'eval'))1           0 BUILD_MAP                03 RETURN_VALUE

list()dict()是单独的对象。它们的名称需要解析,堆栈必须参与推送参数,框架必须存储以供以后检索,并且必须进行调用。这一切都需要更多时间。

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

>>> dis.dis(compile('list()', '', 'eval'))1           0 LOAD_NAME                0 (list)3 CALL_FUNCTION            06 RETURN_VALUE>>> dis.dis(compile('dict()', '', 'eval'))1           0 LOAD_NAME                0 (dict)3 CALL_FUNCTION            06 RETURN_VALUE

您可以使用timeit单独计算名称查找的时间:

>>> import timeit>>> timeit.timeit('list', number=10**7)0.30749011039733887>>> timeit.timeit('dict', number=10**7)0.4215109348297119

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

>>> 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

因此,每次调用对象需要额外的1.00 - 0.31 - 0.30 == 0.39秒1000万调用。

您可以通过将全局名称混淆为本地名称来避免全局查找现象(使用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

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

list()需要全局查找和函数调用,但[]编译为单个指令。请参阅:

Python 2.7.3>>> import dis>>> dis.dis(lambda: list())1           0 LOAD_GLOBAL              0 (list)3 CALL_FUNCTION            06 RETURN_VALUE>>> dis.dis(lambda: [])1           0 BUILD_LIST               03 RETURN_VALUE

这里的答案很好,切中要害,完全涵盖了这个问题。对于那些感兴趣的人,我将从字节码中删除进一步的步骤。我使用的是CPython的最新存储库;旧版本在这方面的行为类似,但可能会有轻微的更改。

以下是其中每一个的执行细分,[]BUILD_LISTlist()CALL_FUNCTION


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();

非常复杂,我知道。事情就是这么简单:

  • 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();

看起来很无害,对吧?嗯,不,不幸的是不是,call_function不是一个会立即调用函数的直截了当的人,它不能。相反,它从堆栈中获取对象,获取堆栈的所有参数,然后根据对象的类型切换;它是一个:

我们调用list类型,传入call_function的参数是PyList_Type。CPython现在必须调用一个泛型函数来处理任何名为_PyObject_FastCallKeywords的可调用对象,还有更多的函数调用。

这个函数再次对某些函数类型进行了一些检查(我不明白为什么),然后在为kwargs如果需要创建了一个字典后,继续调用_PyObject_FastCallDict

_PyObject_FastCallDict终于把我们带到了某个地方!在执行更多的支票之后,我们传入的type中的从#2抓住#1插槽,也就是说,它抓住了type.tp_call。然后它继续从传入_PyStack_AsTuple的参数中创建一个元组,最后,终于可以打电话了

匹配type.__call__tp_call接管并最终创建列表对象。它调用与PyType_GenericNew对应的列表__new__并使用PyType_GenericAlloc为其分配内存:这实际上是它最终赶上第五名的部分。前面的所有内容都是以通用方式处理对象所必需的。

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

最后,记住LOAD_NAME,这是另一个在这里做出贡献的人。


很容易看出,在处理我们的输入时,Python通常必须跳过循环才能真正找到合适的C函数来完成这项工作。它没有立即调用它的礼貌,因为它是动态的,有人可能会屏蔽list和男孩做很多人做)并且必须采用另一条路径。

这就是list()失去很多的地方:探索Python需要做的事情来找出它应该做什么。

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

脚注:所有函数名称都会从一个版本更改到另一个版本。这一点仍然存在,很可能会在任何未来的版本中存在,这是动态查找减慢了速度。

为什么[]list()快?

最大的原因是Python将list()视为用户定义的函数,这意味着你可以通过混淆现象list来拦截它,并做一些不同的事情(比如使用你自己的子类列表或双端队列)。

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

我的解释试图给你这方面的直觉。

补充说明

[]通常称为文字语法。

在语法中,这被称为“列表显示”。从文档里

列表显示是包含在方括号:

list_display ::=  "[" [starred_list | comprehension] "]"

列表显示产生一个新的列表对象,指定内容通过一系列表达或理解。当一个提供逗号分隔的表达式列表,其元素是从左到右评估并放置到列表对象中顺序。当提供理解时,列表是从由理解产生的元素。

简而言之,这意味着创建了一个类型为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.')

然后我们可以在模块级全局作用域上拦截名称list,然后当我们创建list时,我们实际上创建了我们的子类型列表:

>>> list = List>>> a_list = list()List initialized.>>> type(a_list)<class '__main__.List'>

类似地,我们可以将其从全局命名空间中删除

del list

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

import builtinsbuiltins.list = List

现在:

>>> list_0 = list()List initialized.>>> type(list_0)<class '__main__.List'>

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

>>> list_1 = []>>> type(list_1)<class 'list'>

我们可能只是暂时这样做,所以让我们撤消我们的更改-首先从内置中删除新的List对象:

>>> del builtins.list>>> builtins.listTraceback (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

哦,不,我们跟丢了原版。

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

>>> builtins.list = type([])>>> list()[]

那么…

为什么[]list()快?

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

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

调用调用一个可调用的对象(例如,一个函数),可能空的参数序列:

call                 ::=  primary "(" [argument_list [","] | comprehension] ")"

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

>>> import dis>>> dis.dis('list()')1           0 LOAD_NAME                0 (list)2 CALL_FUNCTION            04 RETURN_VALUE>>> dis.dis('doesnotexist()')1           0 LOAD_NAME                0 (doesnotexist)2 CALL_FUNCTION            04 RETURN_VALUE

对于[],在Python字节码级别没有函数调用:

>>> dis.dis('[]')1           0 BUILD_LIST               02 RETURN_VALUE

它只是直接构建列表,而无需在字节码级别进行任何查找或调用。

结论

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

[]是列表显示或文字,因此避免了名称查找和函数调用。