关于性能:元组比Python中的列表更有效吗?

关于性能:元组比Python中的列表更有效吗?

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

不需要复制元组

运行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对象有一个额外的间接层,指向保存两个数据指针的外部数组。


dis模块分解函数的字节代码,并有助于查看元组和列表之间的区别。

在这种情况下,您可以看到访问一个元素会生成相同的代码,但是分配一个元组要比分配一个列表快得多。

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

元组是不可变的,它具有更高的内存效率;为了提高效率,列出了过度分配的内存,以便允许不使用常量reallocs的附加值。因此,如果要在代码中迭代一个常量值序列(例如for direction in 'up', 'right', 'down', 'left':),则首选元组,因为这些元组是在编译时预先计算的。

访问速度应该相同(它们都作为连续数组存储在内存中)。

但是,在处理可变数据时,alist.append(item)atuple+= (item,)更受欢迎。记住,元组被当作没有字段名的记录来处理。


如果列表或元组中的所有项都是相同的C类型,那么还应该考虑标准库中的array模块。它将占用更少的内存,并且速度更快。


元组应该稍微高效一些,因此比列表更快,因为它们是不可变的。


这是另一个小基准,只是为了它。

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确实花费了101.239%的时间,或者1.239%的额外时间来完成这项工作。


元组在读取方面非常有效的主要原因是它是不可变的。

为什么不可变的对象容易读取?

原因是与列表不同,元组可以存储在内存缓存中。程序总是从列表中读取内存位置,因为它是可变的(可以随时更改)。


推荐阅读

    linux性能检测命令?

    linux性能检测命令?,系统,情况,信息,状态,工具,实时,百分比,指标,分析,命令,

    linux查看性能的命令?

    linux查看性能的命令?,系统,情况,信息,数据,状态,指标,第一,分析,命令,宏观,l

    linux磁盘列表命令?

    linux磁盘列表命令?,情况,管理,系统,单位,信息,数据,命令,磁盘,服务,时间,lin

    linux命令筛选列表?

    linux命令筛选列表?,工具,状态,位置,工作,预期,命令,名称,标准,数据,系统,在L

    linux的长列表命令?

    linux的长列表命令?,工作,系统,信息,命令,数据,目录,电脑,软件,时间,设备,Lin

    linux性能测试命令?

    linux性能测试命令?,数据,系统,工具,标准,设备,地址,情况,基础,网络,环境,如

    linux检索文件命令?

    linux检索文件命令?,系统,工作,信息,数据,名称,文件,命令,管理,情况,标准,lin

    linux目录列表命令?

    linux目录列表命令?,系统,信息,标准,工作,命令,地址,时间,数据,名称,目录,lin

    linux命令检索模板?

    linux命令检索模板?,时间,档案,系统,命令,名称,灵活,工具,文件,工作,环境,Lin

    linux命令3d性能?

    linux命令3d性能?,系统,工具,实时,百分比,信息,分析,软件,情况,网站,建设,Lin

    linux性能管理命令?

    linux性能管理命令?,工具,系统,信息,状态,网络,情况,工作,时间,短信,平均,lin

    linux性能调参命令?

    linux性能调参命令?,工具,工作,信息,网络,分析,系统,地址,实时,管理,状态,在l

    linux性能找不打命令?

    linux性能找不打命令?,系统,实时,软件,名字,分析,信息,情况,工具,电脑,时间,l

    Python常见的列表

    Python常见的列表,合法,数据,概念,下来,较大,培训,数组,列表,类型,声明,pyth

    Python 列表(List)

    Python 列表(List),位置,数字,培训,列表,索引,序列,数据项,实例,方括号,元