[英]Why is [] faster than list()?
我最近比较了[]
和list()
的处理速度,惊讶地发现[]
运行速度比list()
快三倍多。 我使用{}
和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 True
和if 1
的性能,似乎触及了类似的文字与全局场景; 也许这也值得考虑。
因为[]
和{}
是字面语法。 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
list()
和dict()
是单独的对象。 它们的名称需要解析,必须涉及堆栈以推送参数,必须存储帧以供稍后检索,并且必须进行调用。 这一切都需要更多的时间。
对于空的情况,这意味着您至少有一个LOAD_NAME
(它必须搜索全局命名空间以及builtins
模块),然后是一个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
您可以使用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
因此,每 1000 万次调用需要额外的1.00 - 0.31 - 0.30 == 0.39
秒。
您可以通过将全局名称别名为本地名称来避免全局查找成本(使用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 0
6 RETURN_VALUE
>>> dis.dis(lambda: [])
1 0 BUILD_LIST 0
3 RETURN_VALUE
由于list
是一个功能转化说一个字符串列表对象,而[]
用于创建一个列表蝙蝠。 试试这个(可能对你更有意义):
x = "wham bam"
a = list(x)
>>> a
["w", "h", "a", "m", ...]
尽管
y = ["wham bam"]
>>> y
["wham bam"]
为您提供一个包含您放入其中的任何内容的实际列表。
这里的答案很好,切中要害,并且完全涵盖了这个问题。 对于那些感兴趣的人,我将进一步降低字节码。 我正在使用最新的 CPython 存储库; 旧版本在这方面的行为类似,但可能会有细微的变化。
下面是对每个执行的分解, BUILD_LIST
用于[]
和CALL_FUNCTION
用于list()
。
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
不是一个会立即调用该函数的直截了当的人,它不能。 相反,它从堆栈中抓取对象,抓取堆栈的所有参数,然后根据对象的类型进行切换; 是不是:
PyCFunction_Type
? 不,它是list
, list
不是PyCFunction
类型PyMethodType
? 没有,请看前面。PyFunctionType
? 不,见前文。 我们正在调用list
类型,传入call_function
的参数是PyList_Type
。 CPython 现在必须调用一个通用函数来处理任何名为_PyObject_FastCallKeywords
可调用对象,还有更多的函数调用。
这个函数再次对某些函数类型进行一些检查(我不明白为什么),然后,如果需要,在为 kwargs 创建一个 dict 之后,继续调用_PyObject_FastCallDict
。
_PyObject_FastCallDict
终于把我们_PyObject_FastCallDict
了某个地方! 在执行更多检查之后,它从我们传入的type
的type
中tp_call
槽,即它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 需要做的事情是找出它到底应该做什么。
另一方面,文字语法意味着一件事。 它无法更改,并且始终以预先确定的方式运行。
脚注:从一个版本到另一个版本,所有函数名称都可能发生变化。 这一点仍然成立,并且很可能在任何未来版本中都会成立,动态查找会减慢速度。
为什么
[]
比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 builtins
builtins.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.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
哦,不,我们忘记了原著。
不用担心,我们仍然可以获取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 0
4 RETURN_VALUE
>>> dis.dis('doesnotexist()')
1 0 LOAD_NAME 0 (doesnotexist)
2 CALL_FUNCTION 0
4 RETURN_VALUE
对于[]
在 Python 字节码级别没有函数调用:
>>> dis.dis('[]')
1 0 BUILD_LIST 0
2 RETURN_VALUE
它只是直接构建列表,无需在字节码级别进行任何查找或调用。
我们已经证明可以使用范围规则通过用户代码拦截list
,并且list()
查找可调用对象然后调用它。
而[]
是列表显示或文字,因此避免了名称查找和函数调用。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.