【问题标题】:Why is tuple faster than list in Python?为什么元组比 Python 中的列表快?
【发布时间】:2011-03-21 09:52:37
【问题描述】:

我刚刚在"Dive into Python" 中读到“元组比列表快”。

tuple 是不可变的,list 是可变的,但是我不太明白为什么 tuple 更快。

有人对此做过性能测试吗?

【问题讨论】:

  • 请编辑您的问题并添加指向您从何处获得此声明的上下文的链接。我什至会好心把它给你,这样你就不用再去找了:diveintopython3.org/native-datatypes.html#tuples
  • 我阅读了 PDF 版本,所以我没有链接。谢谢你:),我会把它添加到问题中
  • 另一方面,您有权质疑此类声明。 Python 解释器在每个版本中都会发生变化;在遵循它们之前,应始终在您自己的平台上对性能声明进行经验验证。 wiki.python.org/moin/PythonSpeed/PerformanceTips Alec Thomas 的回答是一个光辉的例子,说明了如何在未来为自己快速做到这一点。另请参阅 timeit 文档:docs.python.org/library/timeit.html
  • @gotgenes:马上阅读,谢谢 :)

标签: python performance list tuples


【解决方案1】:

本质上是因为元组的不变性意味着解释器可以使用比列表更精简、更快的数据结构。

【讨论】:

    【解决方案2】:

    借助timeit 模块的强大功能,您通常可以自己解决与性能相关的问题:

    $ python2.6 -mtimeit -s 'a = tuple(range(10000))' 'for i in a: pass'
    10000 loops, best of 3: 189 usec per loop
    $ python2.6 -mtimeit -s 'a = list(range(10000))' 'for i in a: pass' 
    10000 loops, best of 3: 191 usec per loop
    

    这表明 tuple 比 list 迭代快得可以忽略不计。我得到了类似的索引结果,但是对于构造,元组破坏了列表:

    $ python2.6 -mtimeit '(1, 2, 3, 4)'   
    10000000 loops, best of 3: 0.0266 usec per loop
    $ python2.6 -mtimeit '[1, 2, 3, 4]'
    10000000 loops, best of 3: 0.163 usec per loop
    

    因此,如果迭代或索引的速度是唯一的因素,则实际上没有区别,但对于构​​造,元组胜出。

    【讨论】:

    • @Vimvq1987 在要求重新编写之前,您是否在 Python 3.0 中尝试过该代码?
    • 几乎没有人使用 Python 3。安装 2.x,不要指望别人为你跳篮球。
    • 我看到元组明显更快的唯一情况是构造它们,这主要是要点——元组被大量用于从函数返回多个值,因此它们针对这种情况进行了优化.通常,使用元组的选择不是基于性能。 (在快速测试中,元组实际上比索引列表慢 20% 左右;我没有费心去研究原因。)
    • 命令行 上运行 Alec 给你的命令(如果你在 Windows 上,则运行 DOS)。
    • 读6年前的cmets“几乎没人用Python 3”哈
    【解决方案3】:

    报告的“构建速度”比率仅适用于 constant 元组(其项目由文字表示的元组)。仔细观察(并在你的机器上重复——你只需要在 shell/命令窗口中输入命令!)...:

    $ python3.1 -mtimeit -s'x,y,z=1,2,3' '[x,y,z]'
    1000000 loops, best of 3: 0.379 usec per loop
    $ python3.1 -mtimeit '[1,2,3]'
    1000000 loops, best of 3: 0.413 usec per loop
    
    $ python3.1 -mtimeit -s'x,y,z=1,2,3' '(x,y,z)'
    10000000 loops, best of 3: 0.174 usec per loop
    $ python3.1 -mtimeit '(1,2,3)'
    10000000 loops, best of 3: 0.0602 usec per loop
    
    $ python2.6 -mtimeit -s'x,y,z=1,2,3' '[x,y,z]'
    1000000 loops, best of 3: 0.352 usec per loop
    $ python2.6 -mtimeit '[1,2,3]'
    1000000 loops, best of 3: 0.358 usec per loop
    
    $ python2.6 -mtimeit -s'x,y,z=1,2,3' '(x,y,z)'
    10000000 loops, best of 3: 0.157 usec per loop
    $ python2.6 -mtimeit '(1,2,3)'
    10000000 loops, best of 3: 0.0527 usec per loop
    

    我没有在 3.0 上进行测量,因为我当然没有它——它已经完全过时了,绝对没有理由保留它,因为 3.1 在各方面都优于它(Python 2.7 ,如果你可以升级到它,在每个任务中测量为比 2.6 快近 20%——正如你所见,2.6 比 3.1 快——所以,如果你认真关心性能,Python 2.7 真的是唯一的释放你应该去的!)。

    无论如何,这里的关键点是,在每个 Python 版本中,使用常量文字构建列表的速度与使用变量引用的值构建列表的速度大致相同,或者稍慢;但是元组的行为非常不同——用常量字面量构建一个元组通常比用变量引用的值构建它的速度快三倍!你可能想知道这是怎么回事,对吧?-)

    答案:由常量文字组成的元组可以很容易地被 Python 编译器识别为一个不可变的常量文字本身:所以它本质上只构建一次,当编译器将源代码转换为字节码并隐藏在相关函数或模块的“常量表”。当这些字节码执行时,它们只需要恢复预先构建的常量元组——嘿,快!-)

    这种简单的优化不能应用于列表,因为列表是一个可变对象,所以如果像[1, 2, 3] 这样的相同表达式执行两次(在循环中——timeit 模块使循环代表您;-),每次都会重新构造一个新的列表对象——并且该构造(例如当编译器无法将其简单地识别为编译时常量和不可变对象时构造元组)确实需要一点时间同时。

    话虽如此,元组构造(当两个构造实际上都必须 发生)仍然是列表构建速度的两倍左右 - 差异可以通过元组的纯粹简单性来解释,其他答案反复提到过。但是,这种简单性并不能解释六倍或更多的加速,正如您观察到的,如果您只将列表和元组的构造与简单的常量文字作为它们的项目进行比较!_)

    【讨论】:

    • 很好的答案,但很难阅读 shell sn-p。请考虑重写或添加汇总表。
    【解决方案4】:

    Alex 给出了一个很好的答案,但我将尝试扩展一些我认为值得一提的事情。任何性能差异通常都很小并且特定于实现:所以不要把农场押在它们身上。

    在 CPython 中,元组存储在单个内存块中,因此创建新元组最多只需要一次调用来分配内存。列表被分配在两个块中:一个包含所有 Python 对象信息的固定块和一个可变大小的数据块。这是创建元组更快的部分原因,但它可能也解释了索引速度的细微差别,因为要跟踪的指针少了一个。

    CPython 中还有一些优化来减少内存分配:取消分配的列表对象保存在空闲列表中,以便可以重复使用,但分配非空列表仍然需要为数据分配内存。元组保存在 20 个不同大小的元组的空闲列表中,因此分配一个小的元组通常根本不需要任何内存分配调用。

    这样的优化在实践中很有帮助,但它们也可能会导致过度依赖“timeit”的结果有风险,当然,如果你转向像 IronPython 这样内存分配工作方式完全不同的东西,它们当然会完全不同。

    【讨论】:

    • “少一个要跟随的指针”——不是这样;虽然内存分配存在差异,但获取特定项目的函数(在去除错误检查后)是相同的:PyObject * PyBLAH_GetItem(PyObject *op, Py_ssize_t i) {return ((PyBLAHObject *)op) -> ob_item[i];}
    • 是的。在元组的数据结构中,ob_item 是结构末尾的数组。在列表中ob_item 是一个指向数组的指针。访问任一数组元素的 C 代码是相同的,但在列表的情况下,需要读取额外的内存来获取指针的值。
    • 你是对的。 tupleobject.h 有PyObject * ob_item[1]; listobject.h 有PyObject ** ob_item;
    【解决方案5】:

    列表明显更快的一个领域是从生成器构造,特别是列表推导比最接近的元组等价物tuple() 具有生成器参数:

    $ python --version
    Python 3.6.0rc2
    $ python -m timeit 'tuple(x * 2 for x in range(10))'
    1000000 loops, best of 3: 1.34 usec per loop
    $ python -m timeit 'list(x * 2 for x in range(10))'
    1000000 loops, best of 3: 1.41 usec per loop
    $ python -m timeit '[x * 2 for x in range(10)]'
    1000000 loops, best of 3: 0.864 usec per loop
    

    特别注意tuple(generator) 似乎比list(generator) 快​​一点点,但[elem for elem in generator] 比它们都快得多。

    【讨论】:

      【解决方案6】:

      元组被 python 编译器识别为一个不可变常量 所以编译器只在哈希表中创建了一个条目并且永远不会改变

      列表是可变对象。所以当我们更新列表时编译器会更新条目 所以它比元组慢一点

      【讨论】:

        【解决方案7】:

        执行摘要

        在几乎所有类别中,元组的性能往往都优于列表

        1) 元组可以是constant folded

        2) 元组可以重复使用而不是复制。

        3) 元组是紧凑的,不会过度分配。

        4) 元组直接引用它们的元素。

        元组可以不断折叠

        常量元组可以由 Python 的窥孔优化器或 AST 优化器预先计算。另一方面,列表是从头开始构建的:

            >>> from dis import dis
        
            >>> dis(compile("(10, 'abc')", '', 'eval'))
              1           0 LOAD_CONST               2 ((10, 'abc'))
                          3 RETURN_VALUE   
        
            >>> dis(compile("[10, 'abc']", '', 'eval'))
              1           0 LOAD_CONST               0 (10)
                          3 LOAD_CONST               1 ('abc')
                          6 BUILD_LIST               2
                          9 RETURN_VALUE 
        

        元组不需要复制

        运行tuple(some_tuple) 会立即返回。由于元组是不可变的,因此不必复制它们:

        >>> a = (10, 20, 30)
        >>> b = tuple(a)
        >>> a is b
        True
        

        相比之下,list(some_list) 要求将所有数据复制到一个新列表中:

        >>> a = [10, 20, 30]
        >>> b = list(a)
        >>> a is b
        False
        

        元组不会过度分配

        由于元组的大小是固定的,它可以比需要过度分配以提高 append() 操作效率的列表更紧凑地存储。

        这给元组一个很好的空间优势:

        >>> import sys
        >>> sys.getsizeof(tuple(iter(range(10))))
        128
        >>> sys.getsizeof(list(iter(range(10))))
        200
        

        这是来自 Objects/listobject.c 的注释,它解释了列表的作用:

        /* This over-allocates proportional to the list size, making room
         * for additional growth.  The over-allocation is mild, but is
         * enough to give linear-time amortized behavior over a long
         * sequence of appends() in the presence of a poorly-performing
         * system realloc().
         * The growth pattern is:  0, 4, 8, 16, 25, 35, 46, 58, 72, 88, ...
         * Note: new_allocated won't overflow because the largest possible value
         *       is PY_SSIZE_T_MAX * (9 / 8) + 6 which always fits in a size_t.
         */
        

        元组直接引用它们的元素

        对对象的引用直接合并到元组对象中。相比之下,列表对外部指针数组有一个额外的间接层。

        这使元组在索引查找和解包方面具有较小的速度优势:

        $ python3.6 -m timeit -s 'a = (10, 20, 30)' 'a[1]'
        10000000 loops, best of 3: 0.0304 usec per loop
        $ python3.6 -m timeit -s 'a = [10, 20, 30]' 'a[1]'
        10000000 loops, best of 3: 0.0309 usec per loop
        
        $ python3.6 -m timeit -s 'a = (10, 20, 30)' 'x, y, z = a'
        10000000 loops, best of 3: 0.0249 usec per loop
        $ python3.6 -m timeit -s 'a = [10, 20, 30]' 'x, y, z = a'
        10000000 loops, best of 3: 0.0251 usec per loop
        

        Here 是元组(10, 20) 的存储方式:

            typedef struct {
                Py_ssize_t ob_refcnt;
                struct _typeobject *ob_type;
                Py_ssize_t ob_size;
                PyObject *ob_item[2];     /* store a pointer to 10 and a pointer to 20 */
            } PyTupleObject;
        

        Here 是列表[10, 20] 的存储方式:

            PyObject arr[2];              /* store a pointer to 10 and a pointer to 20 */
        
            typedef struct {
                Py_ssize_t ob_refcnt;
                struct _typeobject *ob_type;
                Py_ssize_t ob_size;
                PyObject **ob_item = arr; /* store a pointer to the two-pointer array */
                Py_ssize_t allocated;
            } PyListObject;
        

        请注意,元组对象直接合并了两个数据指针,而列表对象对保存两个数据指针的外部数组有一个额外的间接层。

        【讨论】:

        • 很棒的细节。谢谢。 ?
        • 哦,雷蒙德本人!很好的解释。
        【解决方案8】:

        在python中,我们有两种类型的对象。 1。可变,2。不可变
        在 python 中,列表属于可变对象,元组属于不可变对象。

        • 元组存储在单个内存块中。元组是不可变的 所以,它不需要额外的空间来存储新对象。

        • 列表分配在两个块中:固定的一个包含所有 Python 对象信息和可变大小的数据块。

        • 这就是创建元组比列表更快的原因。

        • 这也说明了索引速度的细微差别更快 比列表,因为在用于索引的元组中它遵循的指针更少。

        使用元组的优点:

        • 元组使用更少的内存,而列表使用更多的内存。

        • 我们可以使用字典中的元组作为键,但不能使用 列表。

        • 我们可以通过元组和列表中的索引访问元素。

        元组的缺点:

        • 我们不能向元组添加元素,但可以向列表添加元素。

        • 我们不能对元组进行排序,但在列表中,我们可以通过调用 list.sort() 方法。

        • 我们不能删除元组中的元素,但在列表中,我们可以删除 一个元素。

        • 我们无法替换元组中的元素,但您可以替换列表中的元素。


        Source

        【讨论】:

        • 如果一个元组包含像s = ('a', [1,2,3], 'b')这样的列表,那么如果我们将元素追加到列表中,空间将如何分配? @M.Rostami
        猜你喜欢
        • 1970-01-01
        • 2016-05-15
        • 2011-07-23
        • 2014-10-12
        • 2015-06-29
        • 2010-12-07
        • 2015-09-29
        相关资源
        最近更新 更多