首页 文章

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

提问于
浏览
613

我最近比较了 []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 Trueif 1 的性能进行了比较,似乎触及了类似的字面对全局情景;也许值得考虑一下 .

5 回答

  • 676

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

    最大的原因是Python将 list() 视为用户定义的函数,这意味着你可以通过别名别名来拦截它到 list 并做一些不同的事情(比如使用你自己的子列表或者deque) .

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

    我的解释试图给你直觉 .

    解释

    [] 通常称为文字语法 .

    在语法中,这被称为"list display" . From the docs

    列表显示是一个可能为空的系列表达式,用方括号括起来:list_display :: =“[”[starred_list |理解]“]”
    列表显示产生一个新的列表对象,内容由表达式列表或理解指定 . 当提供以逗号分隔的表达式列表时,其元素将从左到右进行计算,并按该顺序放入列表对象中 . 当提供理解时,列表由理解产生的元素构成 .

    简而言之,这意味着创建了 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() 查找可调用的然后调用它 .

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

  • 133

    因为 []{} 是字面语法 . 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万次调用需要额外的_404411秒 .

    您可以通过将全局名称别名为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
    

    但你永远无法克服成本 .

  • 13

    这里的答案很棒,到了这一点并完全涵盖了这个问题 . 对于那些感兴趣的人,我会从字节码中再下一步 . 我正在使用最新的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不是一个直接调用该函数的直接人,它不能 . 相反,它从堆栈中抓取对象,抓取堆栈的所有参数,然后根据对象的类型进行切换;它是一个:

    我们调用 list 类型,传入 call_function 的参数是PyList_Type . CPython现在必须调用一个泛型函数来处理名为_PyObject_FastCallKeywords的任何可调用对象,以及更多的函数调用 .

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

    _PyObject_FastCallDict 终于让我们到了某个地方!在执行了更多检查之后,grabs the tp_call slot from the type我们已经传入了grabs the tp_call slot from the type,也就是说,它 grab 了 type.tp_call . 然后它继续创建一个由 _PyStack_AsTuple 传递的参数中的元组,最后是a call can finally be made

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

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

    最后,请记住 LOAD_NAME ,这是另一个在这里贡献的人 .


    很容易看出,在处理我们的输入时,Python通常必须跳过箍以实际找到适当的 C 函数来完成这项工作 . 它没有动态,有人可能会掩盖 list (男孩会做很多人这样做),必须采取另一条道路 .

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

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

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

  • 5

    因为 listfunction将一个字符串转换为一个列表对象,而 [] 用于创建一个关闭该列表的列表 . 试试这个(可能对你更有意义):

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

    y = ["wham bam"]
    >>> y
    ["wham bam"]
    

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

  • 72

    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
    

相关问题