我刚刚读过“深入Python”,“元组比列表快”。
元组是不可变的,列表是可变的,但是我不太明白为什么元组更快。
有人对此进行过性能测试吗?
Answers:
所报告的“构建速度”比率仅适用于常量元组(其项目由文字表示的元组)。仔细观察(并在您的机器上重复-您只需要在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
模块代表您执行循环;-),每次都会重新构造一个新的列表对象-并且这种构造(例如当编译器无法简单地将其标识为编译时常量和不可变对象时的元组构造)确实需要一些时间。
话虽这么说,元组构造(实际上必须同时发生两种构造)仍然快于列表构造的两倍-这种差异可以用元组的纯粹性来解释,其他答案也多次提到。但是,这种简单性并不能解决六倍或更多倍的加速问题,正如您观察到的那样,如果您只比较列表和元组的构造以及简单常量文字作为它们的项!_)
亚历克斯给出了一个很好的答案,但我将尝试在一些我认为值得一提的方面进行扩展。通常,任何性能差异都很小,且具体取决于实现方式:因此,不要将服务器场押在它们身上。
在CPython中,元组存储在单个内存块中,因此创建新元组在最坏的情况下涉及到一次分配内存的调用。列表分为两个块:固定块和所有Python对象信息,以及一个可变大小的数据块。这是创建元组更快的部分原因,但它也可能解释了索引速度的细微差异,因为要遵循的指针减少了。
CPython中还有一些优化措施可以减少内存分配:取消分配的列表对象保存在空闲列表中,因此可以重复使用,但是分配非空列表仍然需要为数据分配内存。元组保存在20个空闲列表中,用于不同大小的元组,因此分配一个小元组通常根本不需要任何内存分配调用。
这样的优化在实践中很有用,但是它们也可能使过于依赖'timeit'的结果具有风险,并且如果移至IronPython之类的内存分配工作原理完全不同的地方,则当然完全不同。
PyObject * PyBLAH_GetItem(PyObject *op, Py_ssize_t i) {return ((PyBLAHObject *)op) -> ob_item[i];}
ob_item
中,结构末尾是一个数组。列表中ob_item
是指向数组的指针。用于访问任一数组的元素的C代码是相同的,但是在列表的情况下,需要读取额外的内存以获取指针的值。
PyObject * ob_item[1];
listobject.h具有PyObject ** ob_item;
元组的性能往往比几乎每个类别中的列表都要好:
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的注释,解释了列表在做什么:
/* 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;
请注意,元组对象直接合并了两个数据指针,而列表对象具有指向包含两个数据指针的外部数组的附加间接层。
利用该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
因此,如果迭代或索引的速度是唯一的因素,则实际上没有区别,但是对于构造,元组会获胜。
使用生成器构造列表的地方明显要快得多,特别是列表生成比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]
比两者都快得多。
在python中,我们有两种类型的对象。1.可变的, 2.不可变的。
在python中,列表位于可变对象之下,而元组则位于不可变对象之下。
元组存储在单个内存块中。元组是不可变的,因此不需要额外的空间来存储新对象。
列表分为两个块:固定块和所有Python对象信息,以及一个可变大小的数据块。
这就是创建元组比List更快的原因。
它还解释了索引速度的细微差别比列表快,因为在用于索引的元组中,它跟随的指针更少。
元组是它们使用较少的内存,而列表使用更多的内存。
我们可以在字典中使用元组作为键,但是列表是不可能的。
我们可以在元组和列表中访问带有索引的元素。
我们不能将元素添加到元组,但可以将元素添加到列表。
我们无法对元组进行排序,但是在列表中,我们可以通过调用list.sort()
method进行排序
。
我们无法删除元组中的元素,但是可以在列表中删除元素。
我们不能替换元组中的元素,但是您可以在列表中。