Are tuples more efficient than lists in Python?
当涉及到元素的实例化和检索时,元组和列表之间是否存在性能差异?
一般来说,您可能期望元组速度稍快。但是,您应该明确地测试您的特定情况(如果差异可能影响程序的性能——记住"过早的优化是万恶之源")。
Python让这很容易:时间是你的朋友。
1 2 3 4 5 | $ python -m timeit"x=(1,2,3,4,5,6,7,8)" 10000000 loops, best of 3: 0.0388 usec per loop $ python -m timeit"x=[1,2,3,4,5,6,7,8]" 1000000 loops, best of 3: 0.363 usec per loop |
还有…
1 2 3 4 5 | $ python -m timeit -s"x=(1,2,3,4,5,6,7,8)""y=x[3]" 10000000 loops, best of 3: 0.0938 usec per loop $ python -m timeit -s"x=[1,2,3,4,5,6,7,8]""y=x[3]" 10000000 loops, best of 3: 0.0649 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 |
不需要复制元组
运行
1 2 3 4 | >>> a = (10, 20, 30) >>> b = tuple(a) >>> a is b True |
相比之下,
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
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; |
以下是存储列表
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对象有一个额外的间接层,指向保存两个数据指针的外部数组。
在这种情况下,您可以看到访问一个元素会生成相同的代码,但是分配一个元组要比分配一个列表快得多。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | >>> def a(): ... x=[1,2,3,4,5] ... y=x[2] ... >>> def b(): ... x=(1,2,3,4,5) ... y=x[2] ... >>> import dis >>> dis.dis(a) 2 0 LOAD_CONST 1 (1) 3 LOAD_CONST 2 (2) 6 LOAD_CONST 3 (3) 9 LOAD_CONST 4 (4) 12 LOAD_CONST 5 (5) 15 BUILD_LIST 5 18 STORE_FAST 0 (x) 3 21 LOAD_FAST 0 (x) 24 LOAD_CONST 2 (2) 27 BINARY_SUBSCR 28 STORE_FAST 1 (y) 31 LOAD_CONST 0 (None) 34 RETURN_VALUE >>> dis.dis(b) 2 0 LOAD_CONST 6 ((1, 2, 3, 4, 5)) 3 STORE_FAST 0 (x) 3 6 LOAD_FAST 0 (x) 9 LOAD_CONST 2 (2) 12 BINARY_SUBSCR 13 STORE_FAST 1 (y) 16 LOAD_CONST 0 (None) 19 RETURN_VALUE |
元组是不可变的,它具有更高的内存效率;为了提高效率,列出了过度分配的内存,以便允许不使用常量
访问速度应该相同(它们都作为连续数组存储在内存中)。
但是,在处理可变数据时,
如果列表或元组中的所有项都是相同的C类型,那么还应该考虑标准库中的
元组应该稍微高效一些,因此比列表更快,因为它们是不可变的。
这是另一个小基准,只是为了它。
1 2 3 4 5 | In [11]: %timeit list(range(100)) 749 ns ± 2.41 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each) In [12]: %timeit tuple(range(100)) 781 ns ± 3.34 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each) |
1 2 3 4 5 | In [1]: %timeit list(range(1_000)) 13.5 μs ± 466 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each) In [2]: %timeit tuple(range(1_000)) 12.4 μs ± 182 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each) |
1 2 3 4 5 | In [7]: %timeit list(range(10_000)) 182 μs ± 810 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each) In [8]: %timeit tuple(range(10_000)) 188 μs ± 2.38 μs per loop (mean ± std. dev. of 7 runs, 10000 loops each) |
1 2 3 4 5 | In [3]: %timeit list(range(1_00_000)) 2.76 ms ± 30.5 μs per loop (mean ± std. dev. of 7 runs, 100 loops each) In [4]: %timeit tuple(range(1_00_000)) 2.74 ms ± 31.8 μs per loop (mean ± std. dev. of 7 runs, 100 loops each) |
1 2 3 4 5 | In [10]: %timeit list(range(10_00_000)) 28.1 ms ± 266 μs per loop (mean ± std. dev. of 7 runs, 10 loops each) In [9]: %timeit tuple(range(10_00_000)) 28.5 ms ± 447 μs per loop (mean ± std. dev. of 7 runs, 10 loops each) |
让我们平均一下:
1 2 3 4 5 6 7 8 9 10 11 12 | In [3]: l = np.array([749 * 10 ** -9, 13.5 * 10 ** -6, 182 * 10 ** -6, 2.76 * 10 ** -3, 28.1 * 10 ** -3]) In [2]: t = np.array([781 * 10 ** -9, 12.4 * 10 ** -6, 188 * 10 ** -6, 2.74 * 10 ** -3, 28.5 * 10 ** -3]) In [11]: np.average(l) Out[11]: 0.0062112498000000006 In [12]: np.average(t) Out[12]: 0.0062882362 In [17]: np.average(t) / np.average(l) * 100 Out[17]: 101.23946713590554 |
你可以称之为几乎没有结论。
但是,与列表相比,tuples确实花费了
元组在读取方面非常有效的主要原因是它是不可变的。
为什么不可变的对象容易读取?原因是与列表不同,元组可以存储在内存缓存中。程序总是从列表中读取内存位置,因为它是可变的(可以随时更改)。