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


75

我刚刚读过“深入Python”,“元组比列表快”。

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

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


1
请编辑您的问题,并添加指向您从何处获得此声明的上下文的链接。我什至会好心地把它给您,这样您就不必再去寻找它了:diveintopython3.org/native-datatypes.html#tuples
gotgenes 2010年

我阅读了PDF版本,所以没有链接。谢谢你:),我将它添加到问题中
Quan Mai 2010年

5
另一方面,您有权质疑此类索赔。Python解释器在每个发行版中都会发生变化;在遵循性能声明之前,应始终在自己的平台上根据经验对其进行验证。wiki.python.org/moin/PythonSpeed/PerformanceTips亚历克·托马斯(Alec Thomas)的答案是一个光辉的示例,说明了如何在将来为自己快速地做到这一点。另请参阅使用timeit文档:docs.python.org/library/timeit.html
gotgenes

@gotgenes:立即阅读,谢谢:)
Quan Mai

Answers:


97

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

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

话虽这么说,元组构造(实际上必须同时发生两种构造)仍然快于列表构造的两倍-这种差异可以用元组的纯粹性来解释,其他答案也多次提到。但是,这种简单性并不能解决六倍或更多倍的加速问题,正如您观察到的那样,如果您只比较列表和元组的构造以及简单常量文字作为它们的项!_)


4
很好的答案,但是很难阅读shell片段。请考虑重写或添加摘要表。
Dima Tisnek

28

亚历克斯给出了一个很好的答案,但我将尝试在一些我认为值得一提的方面进行扩展。通常,任何性能差异都很小,且具体取决于实现方式:因此,不要将服务器场押在它们身上。

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

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

这样的优化在实践中很有用,但是它们也可能使过于依赖'timeit'的结果具有风险,并且如果移至IronPython之类的内存分配工作原理完全不同的地方,则当然完全不同。


“少一个要遵循的指针”-不是这样;尽管内存分配有所不同,但获得特定项目的功能(除去错误检查后)是相同的:PyObject * PyBLAH_GetItem(PyObject *op, Py_ssize_t i) {return ((PyBLAHObject *)op) -> ob_item[i];}
John Machin 2010年

8
是的,所以。在元组的数据结构ob_item中,结构末尾是一个数组。列表中ob_item是指向数组的指针。用于访问任一数组的元素的C代码是相同的,但是在列表的情况下,需要读取额外的内存以获取指针的值。
邓肯

3
你是对的。tupleobject.h具有PyObject * ob_item[1];listobject.h具有PyObject ** ob_item;
John Machin

28

执行摘要

元组的性能往往比几乎每个类别中的列表都要好:

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

元组不会过度分配

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

这给元组一个很好的空间优势:

>>> import sys
>>> sys.getsizeof(tuple(iter(range(10))))
128
>>> sys.getsizeof(list(iter(range(10))))
200

这是来自Objects / listobject.c的注释,解释了列表在做什么:

元组直接引用其元素

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

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

$ 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)的存储方式:

这里是列表如何[10, 20]存储:

请注意,元组对象直接合并了两个数据指针,而列表对象具有指向包含两个数据指针的外部数组的附加间接层。


4
很棒的细节。谢谢。🎉–
GollyJer

17

利用该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
@ Vimvq1987在要求重新编写之前,您是否使用Python 3.0尝试过该代码?
gotgenes's

1
几乎没有人使用Python3。安装2.x,不要指望别人为您效劳。
格伦·梅纳德

3
我看到元组明显更快的唯一情况是构造它们,而这在很大程度上是关键–元组经常用于从函数中返回多个值,因此针对该情况对其进行了优化。通常,使用元组的选择不是基于性能的。(在快速测试中,元组实际上比用于索引的列表慢20%;我不必费心研究原因。)
Glenn Maynard 2010年

1
命令行上运行Alec给您的命令(如果使用Windows,则为DOS)。
mechanical_meat 2010年

4
阅读6年前的评论“几乎没人使用Python 3”哈
David Andrei Ned

5

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


2

使用生成器构造列表的地方明显要快得多,特别是列表生成比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]比两者都快得多。


2

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

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

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

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

  • 它还解释了索引速度的细微差别比列表快,因为在用于索引的元组中,它跟随的指针更少。

使用元组的优点:

  • 元组是它们使用较少的内存,而列表使用更多的内存。

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

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

元组的缺点:

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

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

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

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


资源


0

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

列表是可变的对象,因此编译器在更新列表时会更新条目,因此与元组相比要慢一些

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.