为什么 [] 比 list() 快?
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.
我最近发现了“Why is if True slower than if 1?”,它将 if True
的性能与 if 1
的性能进行了比较,似乎触及了类似的文字与全局场景;也许也值得考虑。
因为 list
是一个 function 将字符串转换为列表对象,而 []
用于立即创建列表。试试这个(对你来说可能更有意义):
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 0
3 RETURN_VALUE
>>> dis.dis(compile('{}', '', 'eval'))
1 0 BUILD_MAP 0
3 RETURN_VALUE
list()
和 dict()
是不同的对象。它们的名称需要解析,堆栈必须参与推送参数,帧必须存储以便稍后检索,并且必须进行调用。这一切都需要更多时间。
对于空的情况,这意味着你至少有一个 LOAD_NAME
(which has to search through the global namespace as well as the builtins
module) followed by 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
您可以使用 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
这里的答案很好,切中要害,完全涵盖了这个问题。对于那些感兴趣的人,我将从字节码进一步降低。我正在使用 CPython 的最新回购协议;旧版本在这方面表现相似,但可能会有细微的变化。
以下是每个执行的细分,BUILD_LIST
用于 []
,CALL_FUNCTION
用于 list()
。
The BUILD_LIST
instruction:
你应该看看恐怖:
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
(宏)添加位于堆栈上的任何参数(在我们的例子中不执行)。
怪不得这么快!它是为创建新列表而定制的,没有别的:-)
The CALL_FUNCTION
instruction:
这是您查看代码处理时首先看到的内容 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 now has to call a generic function to handle any callable objects named _PyObject_FastCallKeywords
,是的,更多的函数调用。
此函数再次对某些函数类型进行一些检查(我不明白为什么),然后在为 kwargs 创建字典后 如果需要,继续调用 _PyObject_FastCallDict
.
_PyObject_FastCallDict
终于让我们到达了某个地方!在执行 更多检查后 它 grabs the tp_call
slot from the type
of the type
we've passed in, that is, it grabs type.tp_call
. It then proceeds to create a tuple out of of the arguments passed in with _PyStack_AsTuple
and, finally, a call can finally be made!
tp_call
,匹配type.__call__
takes over and finally creates the list object. It calls the lists __new__
which corresponds to PyType_GenericNew
and allocates memory for it with PyType_GenericAlloc
:这其实是赶上PyList_New
,最后的部分。前面的所有内容都是以通用方式处理对象所必需的。
最后,type_call
调用 list.__init__
并使用任何可用参数初始化列表,然后我们继续原路返回。 :-)
最后,请记住 LOAD_NAME
,这是另一个在这里做出贡献的人。
很容易看出,在处理我们的输入时,Python 通常必须绕圈子才能真正找到合适的 C
函数来完成这项工作。它没有立即调用它的礼貌,因为它是动态的,有人可能会屏蔽 list
( 很多人都这样做 )并且必须采取另一条路径。
这就是 list()
失去很多的地方:探索 Python 需要做的是找出它到底应该做什么。
另一方面,字面语法只表示一件事;它无法更改并且始终以预先确定的方式运行。
脚注:所有函数名称都可能从一个版本更改为另一个版本。这一点仍然存在,并且很可能会在任何未来的版本中存在,它是减慢速度的动态查找。
Why is []
faster than list()
?
最大的原因是 Python 将 list()
视为用户定义的函数,这意味着您可以通过将其他东西别名化为 list
来拦截它并做一些不同的事情(比如使用你自己的子类列表或者双端队列)。
它立即创建一个内置列表的新实例 []
。
我的解释旨在为您提供直觉。
说明
[]
通常称为文字语法。
在语法中,这被称为 "list display"。 From the docs:
A list display is a possibly empty series of expressions enclosed in
square brackets:
list_display ::= "[" [starred_list | comprehension] "]"
A list display yields a new list object, the contents being specified
by either a list of expressions or a comprehension. When a
comma-separated list of expressions is supplied, its elements are
evaluated from left to right and placed into the list object in that
order. When a comprehension is supplied, the list is constructed from
the elements resulting from the 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()
[]
所以...
Why is []
faster than list()
?
正如我们所见 - 我们可以覆盖 list
- 但我们无法拦截文字类型的创建。当我们使用 list
时,我们必须进行查找以查看是否有任何内容。
然后我们必须调用我们查找到的任何可调用对象。来自语法:
A call calls a callable object (e.g., a function) with a possibly
empty series of arguments:
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()
查找可调用对象然后调用它。
而[]
是列表显示,或者说是文字,因此避免了名称查找和函数调用。
我最近比较了 []
和 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.
我最近发现了“Why is if True slower than if 1?”,它将 if True
的性能与 if 1
的性能进行了比较,似乎触及了类似的文字与全局场景;也许也值得考虑。
因为 list
是一个 function 将字符串转换为列表对象,而 []
用于立即创建列表。试试这个(对你来说可能更有意义):
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 0
3 RETURN_VALUE
>>> dis.dis(compile('{}', '', 'eval'))
1 0 BUILD_MAP 0
3 RETURN_VALUE
list()
和 dict()
是不同的对象。它们的名称需要解析,堆栈必须参与推送参数,帧必须存储以便稍后检索,并且必须进行调用。这一切都需要更多时间。
对于空的情况,这意味着你至少有一个 LOAD_NAME
(which has to search through the global namespace as well as the builtins
module) followed by 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
您可以使用 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
这里的答案很好,切中要害,完全涵盖了这个问题。对于那些感兴趣的人,我将从字节码进一步降低。我正在使用 CPython 的最新回购协议;旧版本在这方面表现相似,但可能会有细微的变化。
以下是每个执行的细分,BUILD_LIST
用于 []
,CALL_FUNCTION
用于 list()
。
The BUILD_LIST
instruction:
你应该看看恐怖:
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
(宏)添加位于堆栈上的任何参数(在我们的例子中不执行)。
怪不得这么快!它是为创建新列表而定制的,没有别的:-)
The CALL_FUNCTION
instruction:
这是您查看代码处理时首先看到的内容 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 now has to call a generic function to handle any callable objects named _PyObject_FastCallKeywords
,是的,更多的函数调用。
此函数再次对某些函数类型进行一些检查(我不明白为什么),然后在为 kwargs 创建字典后 如果需要,继续调用 _PyObject_FastCallDict
.
_PyObject_FastCallDict
终于让我们到达了某个地方!在执行 更多检查后 它 grabs the tp_call
slot from the type
of the type
we've passed in, that is, it grabs type.tp_call
. It then proceeds to create a tuple out of of the arguments passed in with _PyStack_AsTuple
and, finally, a call can finally be made!
tp_call
,匹配type.__call__
takes over and finally creates the list object. It calls the lists __new__
which corresponds to PyType_GenericNew
and allocates memory for it with PyType_GenericAlloc
:这其实是赶上PyList_New
,最后的部分。前面的所有内容都是以通用方式处理对象所必需的。
最后,type_call
调用 list.__init__
并使用任何可用参数初始化列表,然后我们继续原路返回。 :-)
最后,请记住 LOAD_NAME
,这是另一个在这里做出贡献的人。
很容易看出,在处理我们的输入时,Python 通常必须绕圈子才能真正找到合适的 C
函数来完成这项工作。它没有立即调用它的礼貌,因为它是动态的,有人可能会屏蔽 list
( 很多人都这样做 )并且必须采取另一条路径。
这就是 list()
失去很多的地方:探索 Python 需要做的是找出它到底应该做什么。
另一方面,字面语法只表示一件事;它无法更改并且始终以预先确定的方式运行。
脚注:所有函数名称都可能从一个版本更改为另一个版本。这一点仍然存在,并且很可能会在任何未来的版本中存在,它是减慢速度的动态查找。
Why is
[]
faster thanlist()
?
最大的原因是 Python 将 list()
视为用户定义的函数,这意味着您可以通过将其他东西别名化为 list
来拦截它并做一些不同的事情(比如使用你自己的子类列表或者双端队列)。
它立即创建一个内置列表的新实例 []
。
我的解释旨在为您提供直觉。
说明
[]
通常称为文字语法。
在语法中,这被称为 "list display"。 From the docs:
A list display is a possibly empty series of expressions enclosed in square brackets:
list_display ::= "[" [starred_list | comprehension] "]"
A list display yields a new list object, the contents being specified by either a list of expressions or a comprehension. When a comma-separated list of expressions is supplied, its elements are evaluated from left to right and placed into the list object in that order. When a comprehension is supplied, the list is constructed from the elements resulting from the 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()
[]
所以...
Why is
[]
faster thanlist()
?
正如我们所见 - 我们可以覆盖 list
- 但我们无法拦截文字类型的创建。当我们使用 list
时,我们必须进行查找以查看是否有任何内容。
然后我们必须调用我们查找到的任何可调用对象。来自语法:
A call calls a callable object (e.g., a function) with a possibly empty series of arguments:
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()
查找可调用对象然后调用它。
而[]
是列表显示,或者说是文字,因此避免了名称查找和函数调用。