问题:为什么Python的数组变慢?
我期望array.array
比列表更快,因为数组似乎已拆箱。
但是,我得到以下结果:
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
造成这种差异的原因是什么?
回答 0
该存储是“未装箱的”,但是每次访问元素时,Python都必须对其进行“装箱”(将其嵌入到常规Python对象中),以便对其进行任何处理。例如,您sum(A)
遍历数组,并将每个整数一次装在一个常规Python int
对象中。那要花时间。在您的中sum(L)
,所有装箱操作均在创建列表时完成。
因此,最后,阵列通常较慢,但所需的内存却少得多。
这是最新版本的Python 3的相关代码,但是自Python首次发布以来,相同的基本思想也适用于所有CPython实现。
这是访问列表项的代码:
PyObject *
PyList_GetItem(PyObject *op, Py_ssize_t i)
{
/* error checking omitted */
return ((PyListObject *)op) -> ob_item[i];
}
它几乎没有什么: somelist[i]
只返回i
列表中的第一个对象(CPython中的所有Python对象都是指向结构的指针,该结构的初始段符合a的布局struct PyObject
)。
这__getitem__
是array
带有类型代码的实现l
:
static PyObject *
l_getitem(arrayobject *ap, Py_ssize_t i)
{
return PyLong_FromLong(((long *)ap->ob_item)[i]);
}
原始内存被视为平台本地C
long
整数的向量;该i
“个C long
被读取起来; 然后PyLong_FromLong()
调用该方法将本机包装(“框”)C long
在Python long
对象中(在Python 3中,该对象消除了Python 2 int
和之间的区别long
,实际上显示为type int
)。
此装箱必须为Python int
对象分配新的内存,然后将native C long
的位喷入其中。在原始示例的上下文中,该对象的生存期非常短(足够长,足以sum()
将内容添加到运行总计中),然后需要更多时间才能取消分配新int
对象。
在CPython实现中,这就是速度差异的来源,永远都是这样,永远都是这样。
回答 1
为了增加蒂姆·彼得斯的出色答案,数组实现了缓冲协议,而列表则没有。这意味着,如果您正在编写C扩展名(或道德上的对等物,例如编写Cython模块),则可以比Python更快地访问和使用数组的元素。这将为您带来可观的速度改进,可能远远超过一个数量级。但是,它有很多缺点:
- 您现在正从事编写C而不是Python的工作。Cython是改善这种情况的一种方法,但是并不能消除语言之间的许多基本差异;您需要熟悉C语义并了解它在做什么。
- PyPy的C API 在某种程度上可以正常工作,但是速度不是很快。如果您以PyPy为目标,则可能应该只编写带有常规列表的简单代码,然后让JITter为您优化它。
- C扩展比纯Python代码更难分发,因为它们需要编译。编译通常取决于体系结构和操作系统,因此您需要确保要针对目标平台进行编译。
直接转到C扩展程序可能会使用大锤拍打苍蝇,具体取决于您的用例。您首先应该研究NumPy,看看它是否足够强大,可以执行您想做的任何数学运算。如果正确使用,它将比本地Python快得多。
回答 2
蒂姆·彼得斯(Tim Peters)回答了为什么这样做很慢,但让我们看看如何改善它。
坚持您的示例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也需要装箱/拆箱,这有额外的开销。要使其快速运行,必须停留在numpy C代码内:
%timeit N.sum()
100 loops, best of 3: 6.27 ms per loop
因此,从列表解决方案到numpy版本,这是运行时的因素16。
我们还要检查创建这些数据结构需要花费多长时间
%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
还要注意,创建数据结构所花费的时间与求和所花费的时间差不多,甚至更多。分配内存很慢。
那些的内存使用情况:
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
但是事实证明,在我的计算机上添加64位整数要快于32位整数,因此只有在受内存/带宽限制的情况下才值得这样做。
回答 3
请注意,100000000
等于10^8
不等于10^7
,我的结果如下所示:
100000000 == 10**8
# my test results on a Linux virtual machine:
#<L = list(range(100000000))> Time: 0:00:03.263585
#<A = array.array('l', range(100000000))> Time: 0:00:16.728709
#<L = list(range(10**8))> Time: 0:00:03.119379
#<A = array.array('l', range(10**8))> Time: 0:00:18.042187
#<A = array.array('l', L)> Time: 0:00:07.524478
#<sum(L)> Time: 0:00:01.640671
#<np.sum(L)> Time: 0:00:20.762153