Python 中元组比列表更高效吗?
-
09-06-2019 - |
题
在元素的实例化和检索方面,元组和列表之间是否存在性能差异?
解决方案
这 dis
模块反汇编函数的字节代码,对于查看元组和列表之间的差异很有用。
在这种情况下,您可以看到访问元素会生成相同的代码,但分配元组比分配列表要快得多。
>>> 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
其他提示
一般来说,您可能期望元组会稍微快一些。但是,您绝对应该测试您的具体情况(如果差异可能会影响程序的性能 - 请记住“过早的优化是万恶之源”)。
Python 使这变得非常简单: 计时 是你的朋友。
$ 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
和...
$ 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 优化器预先计算。另一方面,列表是从头开始构建的:
>>> 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
元组不会过度分配
由于元组的大小是固定的,因此它可以比需要过度分配才能存储的列表更紧凑地存储 附加() 运营高效。
这为元组提供了很好的空间优势:
>>> import sys
>>> sys.getsizeof(tuple(iter(range(10))))
128
>>> sys.getsizeof(list(iter(range(10))))
200
这是来自的评论 对象/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
这里 元组是怎样的 (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;
这里 名单是这样的 [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;
请注意,元组对象直接合并两个数据指针,而列表对象具有到保存两个数据指针的外部数组的附加间接层。
元组是不可变的,因此内存效率更高;列表,为了效率,过度分配内存以允许没有常量的附加 realloc
s。因此,如果您想迭代代码中的恒定值序列(例如 for direction in 'up', 'right', 'down', 'left':
),元组是首选,因为此类元组是在编译时预先计算的。
访问速度应该相同(它们都作为连续数组存储在内存中)。
但, alist.append(item)
更倾向于 atuple+= (item,)
当你处理可变数据时。请记住,元组应被视为没有字段名称的记录。
您还应该考虑 array
如果列表或元组中的所有项目都属于相同的 C 类型,则标准库中的模块。它将占用更少的内存并且速度更快。
元组应该比列表稍微高效一些,并且速度更快,因为它们是不可变的。
这是另一个小基准,只是为了它..
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)
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)
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)
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)
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)
让我们对它们进行平均:
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
你可以说它几乎是不确定的。
但可以肯定的是,元组采取了 101.239%
时间,或者 1.239%
与清单相比,有更多的时间来完成这项工作。
Tuple 读取非常高效的主要原因是它是不可变的。
为什么不可变对象易于阅读?
原因是元组可以存储在内存缓存中,与列表不同。程序始终从列表内存位置读取,因为它是可变的(可以随时更改)。