关于python:为什么[]比list()快?

Why is [] faster than list()?

我最近比较了[]list()的处理速度,惊讶地发现[]list()快三倍多。我用{}dict()进行了相同的测试,结果几乎相同:[]{}都用了大约0.128sec/million个周期,而list()dict()每个周期大约用了0.428sec/million个周期。

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

我不知道这两种方法有什么不同,但我想知道。我在文件中找不到答案,找空括号的问题比我想象的要多。

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

我最近发现"为什么如果真的比1慢?"这比较了if Trueif 1的表现,似乎涉及到一个类似的字面上的和全球的情况;也许也值得考虑。


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

1
2
3
4
5
6
7
>>> 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(它必须通过全局命名空间和__builtin__模块进行搜索),然后是一个CALL_FUNCTION,它必须保留当前帧:

1
2
3
4
5
6
7
8
>>> 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单独计时名称查找:

1
2
3
4
5
>>> import timeit
>>> timeit.timeit('list', number=10**7)
0.30749011039733887
>>> timeit.timeit('dict', number=10**7)
0.4215109348297119

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

1
2
3
4
5
6
7
8
>>> 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设置,绑定到名称的所有内容都是本地的):

1
2
3
4
5
6
7
8
>>> 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

但你永远无法克服这一点。


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

1
2
3
4
5
6
7
8
9
10
11
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


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

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

同时

1
2
3
y = ["wham bam"]
>>> y
["wham bam"]

给你一个包含你放在里面的任何东西的实际列表。


这里的答案是很好的,就这点而言,并完全涵盖了这个问题。对于那些感兴趣的人,我将从字节代码中进一步降低一步。我正在使用最新的cpython回购;在这方面,旧版本的行为类似,但可能会有细微的变化。

以下是对每一项的执行情况的分解,BUILD_LIST代表[]CALL_FUNCTION代表list()

BUILD_LIST指令:

你应该看看恐怖:

1
2
3
4
5
6
7
8
9
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时看到的第一件事:

1
2
3
4
5
6
7
8
9
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号?不,它是listlist不是PyCFunction型。
  • PyMethodType号?不,见前面。
  • PyFunctionType号?不,看前面。

我们称之为list类型,传递给CALL_FUNCTION的参数是PyList_Type。cpython现在必须调用一个通用函数来处理任何名为_PyObject_FastCallKeywords的可调用对象,还需要更多的函数调用。

此函数再次检查某些函数类型(我无法理解原因),然后在为Kwargs创建dict(如果需要)后,继续调用_PyObject_FastCallDict

_PyObject_FastCallDict终于把我们带到了某个地方!在执行更多的检查之后,它从我们通过的typetypetp_call槽中抓取type.tp_call。然后,它根据与_PyStack_AsTuple一起传入的参数创建一个元组,最后,可以进行调用!

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

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

最后,Remmeber the LOAD_NAME,这是另一个贡献于此的人。

很容易看出,在处理我们的输入时,python通常必须跳过Hoops,以便实际找到合适的C函数来完成这项工作。它没有立即调用它的短促,因为它是动态的,有人可能会屏蔽list(男孩做很多人做的),必须采取另一条路径。

这就是list()损失惨重的地方:探索python需要做的是找出它应该做什么。

另一方面,字面句法只意味着一件事;它不能被改变,并且总是以预先确定的方式运行。

footnote:all function name are subject to change from one one to the other.这一点仍然存在,而且很可能在未来的任何版本中都存在,是动态查找减慢了速度。


Why is [] faster than list()?

最大的原因是,python将list()视为用户定义的函数,这意味着您可以通过将其他东西别名为list来截取它,并做一些不同的事情(例如使用自己的子类列表或deque)。

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

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

解释

[]通常被称为字面语法。

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

A list display is a possibly empty series of expressions enclosed in
square brackets:

1
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可以尽可能快地完成它。

另一方面,通过使用builtin list构造函数创建一个内置的list,可以截获list()

例如,假设我们希望大声创建列表:

1
2
3
4
5
6
7
class List(list):
    def __init__(self, iterable=None):
        if iterable is None:
            super().__init__()
        else:
            super().__init__(iterable)
        print('List initialized.')

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

1
2
3
4
5
>>> list = List
>>> a_list = list()
List initialized.
>>> type(a_list)
<class '__main__.List'>

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

1
del list

并将其放入内置命名空间:

1
2
import builtins
builtins.list = List

现在:

1
2
3
4
>>> list_0 = list()
List initialized.
>>> type(list_0)
<class '__main__.List'>

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

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

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

1
2
3
4
5
6
7
8
9
>>> 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—它是一个列表文本的类型:

1
2
3
>>> 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:

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

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

1
2
3
4
5
6
7
8
9
>>> 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字节码级别没有函数调用:

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

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

结论

我们已经证明,使用作用域规则可以使用用户代码截取list,并且list()查找可调用文件,然后调用它。

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