为什么 [] 比 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 不是一个会立即调用该函数的直截了当的家伙,它不能。相反,它从堆栈中抓取对象,抓取堆栈的所有参数,然后根据对象的类型进行切换;是吗:

我们正在调用 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() 查找可调用对象然后调用它。

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