为什么元组比Python中的列表更快?

时间:2010-07-27 03:26:57

标签: python performance list tuples

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

元组是不可变的,列表是可变的,但我不太明白为什么元组更快。

有人对此进行了性能测试吗?

8 个答案:

答案 0 :(得分:84)

报告的“构造速度”比率仅适用于常量元组(其项目由文字表示的元组)。仔细观察(并在您的机器上重复 - 您只需要在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编译器识别为一个不可变的常量文字本身:所以它基本上只构建一次,当编译器将源转换为字节码时,并隐藏在相关函数或模块的“常量表”。当这些字节码执行时,他们只需要恢复预先构建的常量元组 - 嘿presto! - )

这种简单的优化不能应用于列表,因为列表是一个可变对象,因此,如果同一个表达式(如[1, 2, 3])执行两次(在循环中 - timeit则至关重要模块代表你做循环;-),每次重新构造一个全新的列表对象 - 和构造(如编译器不能轻易地将其识别为编译时常量和不可变对象时构造元组)确实需要一段时间。

话虽如此,元组结构(当两个结构实际上都必须 仍然是列表构造的两倍 - 而 差异可以通过元组的简单性来解释,其他答案已经反复提到过。但是,这种简单性并没有考虑到六倍或更多的加速,正如你观察到的那样,你只是将列表和元组的结构与简单的常量文字作为项目进行比较!_)

答案 1 :(得分:18)

亚历克斯给出了一个很好的答案,但我会尝试扩展一些我认为值得一提的事情。任何性能差异通常都很小并且具体实现:所以不要在农场上赌它们。

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

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

这样的优化在实践中是有帮助的,但它们也可能使得过多地依赖于'timeit'的结果存在风险,当然如果你转向像IronPython那样内存分配工作方式完全不同的东西则完全不同。 / p>

答案 2 :(得分:16)

凭借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

这表明元组的迭代速度可以忽略不计。我得到类似的索引结果,但对于构​​造,元组破坏列表:

$ 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

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

答案 3 :(得分:11)

执行摘要

在几乎每个类别中,元组往往比列表表现更好:

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;

请注意,元组对象直接包含两个数据指针,而列表对象有另外一层间接到包含两个数据指针的外部数组。

答案 4 :(得分:5)

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

答案 5 :(得分:1)

列表明显更快的一个区域是生成器的构造,特别是,列表推导比具有生成器参数的最接近的元组等价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]要快于.isin()

答案 6 :(得分:1)

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

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

  • 列表分为两个块:固定的所有Python块 对象信息和数据的可变大小块。

  • 这是创建元组比List更快的原因。

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

使用元组的优势:

  • 问题在于,它们使用的内存更少,而列表使用的内存更多。

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

  • 我们可以在元组和列表中访问带有索引的元素。

元组的缺点:

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

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

  • 我们无法删除元组中的元素,但是在列表中,我们可以删除 元素。

  • 我们不能替换元组中的元素,但可以替换列表中的元素。


Source

答案 7 :(得分:0)

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

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