为什么Python的数组变慢?

我期望array.array比列表更快,因为数组似乎是unboxed。

但是,我得到以下结果:

 In [1]: import array In [2]: L = list(range(100000000)) In [3]: A = array.array('l', range(100000000)) In [4]: %timeit sum(L) 1 loop, best of 3: 667 ms per loop In [5]: %timeit sum(A) 1 loop, best of 3: 1.41 s per loop In [6]: %timeit sum(L) 1 loop, best of 3: 627 ms per loop In [7]: %timeit sum(A) 1 loop, best of 3: 1.39 s per loop 

造成这种差别的原因是什么?

存储是“拆箱”的,但是每次访问一个元素时,Python都必须将其“embedded”(embedded到一个普通的Python对象中),以便对其进行处理。 例如,您的sum(A)遍历数组,并将每个整数(每次一个)放在常规的Python int对象中。 这花费时间。 在你的sum(L) ,所有的拳击都是在列表创build的时候完成的。

所以,最后,一个数组通常比较慢,但是需要的内存要less得多。


以下是最近版本的Python 3中的相关代码,但自从Python首次发布以来,所有CPython实现都适用于相同的基本思想。

以下是访问列表项的代码:

 PyObject * PyList_GetItem(PyObject *op, Py_ssize_t i) { /* error checking omitted */ return ((PyListObject *)op) -> ob_item[i]; } 

有很less的东西: somelist[i]只是返回列表中的第i个对象(并且CPython中的所有Python对象都是指向初始段符合struct PyObject的布局的结构的指针)。

以下是types代码为larray__getitem__实现:

 static PyObject * l_getitem(arrayobject *ap, Py_ssize_t i) { return PyLong_FromLong(((long *)ap->ob_item)[i]); } 

原始内存被视为平台本地C long整数的向量; iC long读起来; 然后PyLong_FromLong()来将Python long对象(在Python 3中消除了Python 2与int之间的区别,实际上显示为inttypes)中的本地C long包装(“框”)。

这个拳击必须为一个Python int对象分配新的内存,并且将本地的C long的比特喷入它。 在原始示例的上下文中,此对象的生命周期非常短(只要sum()将内容添加到正在运行的总计中足够长),然后需要更多时间来释放新的int对象。

这是速度差异的来源,总是来自于CPython的实现。

为了增加Tim Peters的出色答案,数组实现了缓冲区协议 ,而列表则没有。 这意味着, 如果你正在编写一个C扩展 (或者等同于编写一个Cython模块的道德等价物),那么你就可以比Python所能做的更快地访问和使用数组的元素。 这会给你很大的速度提升,甚至可能超过一个数量级。 然而,它有一些缺点:

  1. 你现在正在写C而不是Python。 Cython是改善这种情况的一种方法,但它并不能消除语言之间的许多根本性差异; 你需要熟悉C语义并理解它在做什么。
  2. PyPy的C API 在某种程度上起作用,但不是很快。 如果您的目标是PyPy,那么您应该只需使用常规列表编写简单的代码,然后让JITter为您进行优化。
  3. C扩展比纯Python代码更难分发,因为它们需要被编译。 编译往往是架构和操作系统的依赖,所以你将需要确保你正在编译你的目标平台。

直接到C扩展可能会使用大锤来打苍蝇,这取决于您的使用情况。 你应该首先调查NumPy ,看看它是否足够强大,可以做任何你想做的事情。 如果使用正确的话,它也会比本机Python快得多。

蒂姆·彼得斯回答了为什么这是缓慢的,但让我们看看如何改善它。

坚持你的范例sum(range(...)) (比你的例子小10倍,以适应内存):

 import numpy import array L = list(range(10**7)) A = array.array('l', L) N = numpy.array(L) %timeit sum(L) 10 loops, best of 3: 101 ms per loop %timeit sum(A) 1 loop, best of 3: 237 ms per loop %timeit sum(N) 1 loop, best of 3: 743 ms per loop 

这种方式也numpy需要box / unbox,这有额外的开销。 为了让它快速,必须保持在numpy的c代码中:

 %timeit N.sum() 100 loops, best of 3: 6.27 ms per loop 

所以从列表解决scheme到numpy版本,这在运行时是16的因素。

我们来看看创build这些数据结构需要多长时间

 %timeit list(range(10**7)) 1 loop, best of 3: 283 ms per loop %timeit array.array('l', range(10**7)) 1 loop, best of 3: 884 ms per loop %timeit numpy.array(range(10**7)) 1 loop, best of 3: 1.49 s per loop %timeit numpy.arange(10**7) 10 loops, best of 3: 21.7 ms per loop 

明确赢家:Numpy

另外请注意,创build数据结构所需的时间与总结时间相同,如果不是更多的话。 分配内存很慢。

内存使用情况如下:

 sys.getsizeof(L) 90000112 sys.getsizeof(A) 81940352 sys.getsizeof(N) 80000096 

所以这些每个数字8个字节不同的开销。 对于我们使用32位整数的范围是足够的,所以我们可以安全地存储一些内存。

 N=numpy.arange(10**7, dtype=numpy.int32) sys.getsizeof(N) 40000096 %timeit N.sum() 100 loops, best of 3: 8.35 ms per loop 

但事实certificate,在我的机器上添加64位整数比32位整数要快,所以如果受内存/带宽的限制,这是唯一值得的。