为什么[]比list()更快?

时间:2015-05-13 13:16:23

标签: python performance list instantiation literals

我最近比较了[]list()的处理速度,并惊讶地发现[] 运行的速度比{{1}快 }。我使用list(){}进行了相同的测试,结果几乎完全相同:dict()[]两者都花费了大约0.128秒/百万个周期,而{}并且list()每个大约需要0.428秒/百万次。

这是为什么? dict()[](以及可能还有{}())会立即传回一些空库存文字的副本,而其明确命名的对应文件({{1} },''list()dict())完全去创建一个对象,无论它们是否真的有元素?

我不知道这两种方法有何不同,但我很想知道。 我在文档中或在SO上找不到答案,搜索空括号结果比我预期的问题更多。

我通过调用tuple()str()以及timeit.timeit("[]")timeit.timeit("list()")来分别比较列表和词典,从而获得了时间结果。我正在运行Python 2.7.9。

我最近发现“Why is if True slower than if 1?”比较了timeit.timeit("{}")timeit.timeit("dict()")的效果,似乎触及了类似的字面与全局情景;也许它值得考虑。

4 个答案:

答案 0 :(得分:707)

因为[]{}文字语法。 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(必须搜索全局命名空间以及__builtin__ module)后跟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秒。

您可以通过将全局名称别名为locals来避免全局查找成本(使用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费用。

答案 1 :(得分:138)

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

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

答案 2 :(得分:73)

因为SELECT t.error_code , t.error_description , t.code FROM temp_soap_monitoring_topup sm , XMLTABLE( XMLNAMESPACES ( 'http://schemas.xmlsoap.org/soap/envelope/' AS "soapenv", 'http://service.soap.CDRator.com' as "ns", 'http://core.data.soap.CDRator.com/xsd' as "ax2130", 'http://webshop.result.service.soap.CDRator.com/xsd' as "ax2147", 'http://core.signup.data.soap.CDRator.com/xsd' as "ns3", 'http://service.soap.CDRator.com' as "ns5", 'http://payment.result.service.soap.CDRator.com/xsd' as "ax230", 'http://core.data.soap.CDRator.com/xsd' as "ax233", 'http://core.result.service.soap.CDRator.com/xsd' as "ax232" ) , 'soapenv:Envelope/soapenv:Body/ns:updateRechargeTicketResponse/ns:return' PASSING XMLTYPE(sm.response_xml) COLUMNS error_code VARCHAR2(100) PATH 'ax232:code/text()' , error_description VARCHAR2(100) PATH 'ax232:description/text()' , code VARCHAR2(100) PATH 'ax230:rechargeTicket/ax233:code/text()' ) t 是要将字符串转换为列表对象的function,而list用于创建一个关键字列表。试试这个(可能对你更有意义):

[]

虽然

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

为您提供包含您放入其中的任何内容的实际列表。

答案 3 :(得分:18)

这里的答案很棒,非常重要,完全涵盖了这个问题。对于那些感兴趣的人,我将从字节码中再下一步。我使用了最新的CPython回购;旧版本在这方面表现相似,但可能会有细微的变化。

以下是BUILD_LIST的{​​{1}}和[] CALL_FUNCTION的执行细分。

The BUILD_LIST instruction:

你应该只看恐怖:

list()

我知道,非常复杂。这很简单:

  • 使用PyList_New创建一个新列表(这主要为新列表对象分配内存),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(); 表示堆栈中的参数数量。直截了当。
  • 检查oparg没有出错。
  • 使用PyList_SET_ITEM(宏)添加位于堆栈上的任何参数(在我们的示例中,这不是执行)。

难怪它快!它是为创建新列表而定制的,没有别的: - )

The CALL_FUNCTION instruction:

这是您在查看代码处理if (list==NULL)时看到的第一件事:

CALL_FUNCTION

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

  • PyCFunction_Type?不,它是PyObject **sp, *res; sp = stack_pointer; res = call_function(&sp, oparg, NULL); stack_pointer = sp; PUSH(res); if (res == NULL) { goto error; } DISPATCH(); list不属于list
  • 类型
  • PyMethodType?不,见上一页。
  • PyFunctionType?不,见上一页。

我们正在调用PyCFunction类型,传递给list的参数为PyList_Type。 CPython现在必须调用泛型函数来处理任何名为_PyObject_FastCallKeywords的可调用对象,并且更多函数调用。

这个函数再次对某些函数类型进行了一些检查(我无法理解为什么)然后,在为kwargs 创建一个dict后,如果需要,继续调用_PyObject_FastCallDict。< / p>

call_function终于让我们到了某个地方!执行了更多检查后,_PyObject_FastCallDict我们传入了type,即抓取type.tp_call。然后,它继续使用_PyStack_AsTuple传递的参数创建一个元组,最后, grabs the tp_call slot from the type

匹配a call can finally be made

tp_call接管并最终创建列表对象。它调用与type.__call__对应的列表__new__,并为PyType_GenericNew分配内存:这实际上是它追赶PyList_New的部分,最后< / em>的。所有以前的都是以通用方式处理对象所必需的。

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

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

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

这是list()失去的地方:探索Python需要做的是找出它应该做些什么。

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

脚注:所有功能名称可能会从一个版本更改为另一个版本。这一点仍然存在,并且很可能会出现在任何未来的版本中,它是动态查找,会减慢速度。