关于性能:为什么元组比Python中的列表更快?

Why is tuple faster than list in Python?

我刚刚在"深入到Python"中读到"元组比列表更快"。

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

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


报告的"构建速度"比率仅适用于常量元组(其项用文本表示的元组)。仔细观察(在您的机器上重复——您只需要在shell/command窗口中键入命令!)……

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ 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,如果可以升级到3.0,每个任务中的测量速度比2.6快20%——而2.6,如您所见,比3.1快——所以,如果您认真关注性能,python 2.7确实是您应该关注的唯一版本!).

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

答:由常量文本构成的元组很容易被python编译器识别为一个不可变的常量文本本身:因此,当编译器将源代码转换为字节码,并存储在相关函数或模块的"常量表"中时,它基本上只构建一次。当这些字节码执行时,它们只需要恢复预先构建的常量元组——嘿,presto!-)

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

也就是说,元组构造(当两个构造实际上都必须仍然是列表构造速度的两倍——这种差异可以用元组的简单性来解释,其他答案也反复提到过这种简单性。但是,这种简单性并不能解释6倍或更多的加速,正如您观察到的那样,如果您只将列表和元组的构造与简单的常量文本作为它们的项进行比较的话!()


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

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

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

像这样的优化在实践中是有用的,但是它们也可能会使过分依赖"timeit"的结果变得危险,当然,如果您转向像Ironpython这样的内存分配工作方式完全不同的东西,那么这是完全不同的。


利用timeit模块的功能,您通常可以自己解决与性能相关的问题:

1
2
3
4
$ 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。我得到了相似的索引结果,但是对于构造,tuple会破坏列表:

1
2
3
4
$ 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

因此,如果迭代或索引的速度是唯一的因素,那么实际上没有区别,但是对于构造来说,元组是成功的。


执行摘要

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

1)元组可以连续折叠。

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

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

4)元组直接引用其元素。

元组可以连续折叠

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

1
2
3
4
5
6
7
8
9
10
11
    >>> 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)会立即返回自身。因为元组是不可变的,所以不必复制它们:

1
2
3
4
>>> a = (10, 20, 30)
>>> b = tuple(a)
>>> a is b
True

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

1
2
3
4
>>> a = [10, 20, 30]
>>> b = list(a)
>>> a is b
False

。元组不会过度分配

因为元组的大小是固定的,所以它可以比需要过度分配才能使append()操作高效的列表更紧凑地存储。

这给了tuples一个很好的空间优势:

1
2
3
4
5
>>> import sys
>>> sys.getsizeof(tuple(iter(range(10))))
128
>>> sys.getsizeof(list(iter(range(10))))
200

下面是来自objects/listobject.c的注释,它解释了列表正在执行的操作:

1
2
3
4
5
6
7
8
9
/* 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.
 */

。元组直接引用其元素

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

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

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

以下是tuple (10, 20)的存储方式:

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

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

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

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


本质上是因为tuple的不变性意味着与list相比,解释器可以使用更精简、更快的数据结构。


列表的一个显著更快的领域是从生成器构造,特别是,列表的理解比最接近的元组等价物tuple()和生成器参数快得多:

1
2
3
4
5
6
7
8
$ 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]比两者都快得多。


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

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