为什么 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

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

22418 次浏览

储藏室是“ unbox”的,但是每次访问元素时,Python 都必须“ box”它(将它嵌入到常规 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’th 对象(CPython 中的所有 Python 对象都是指向一个结构的指针,该结构的初始段符合 struct PyObject的布局)。

下面是 array__getitem__实现,类型代码为 l:

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

原始内存被视为平台本机 C long整数的向量; 读取 i‘ th C long; 然后调用 PyLong_FromLong()在 Python long对象中包装(“ box”)本机 C long(在 Python 3中,它消除了 Python 2在 intlong之间的区别,实际上显示为类型 int)。

这个装箱过程必须为 Pythonint对象分配新的内存,并将本机 C long的位元插入其中。在原始示例的上下文中,这个对象的生命周期非常短(刚好够 sum()将内容添加到运行的总数中) ,然后需要更多的时间来释放新的 int对象。

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

为了增加 Tim Peters 的出色答案,数组实现了 缓冲协议,而列表没有。这意味着,如果你正在写一个 C 扩展(或道德上的等价物,例如编写 Cython模块)可以比 Python 更快地访问和处理数组的元素。这将给你带来相当大的速度提升,可能远远超过一个数量级。然而,它也有许多缺点:

  1. 您现在从事的工作是编写 C 语言而不是 Python。Cython 是改进这一点的一种方法,但它并不能消除语言之间的许多基本差异; 您需要熟悉 C 语义并理解它在做什么。
  2. PyPy 的 CAPI 可以工作在 在某种程度上上,但是速度不是很快。如果您的目标是 PyPy,那么您可能只需要编写带有常规列表的简单代码,然后让 JITter 为您优化它。
  3. C 扩展比纯 Python 代码更难分发,因为它们需要编译。编译往往依赖于体系结构和操作系统,因此需要确保为目标平台进行编译。

直接使用 C 扩展可能会使用大锤击打苍蝇,这取决于您的用例。您应该首先研究 笨蛋,看看它是否足够强大,可以做任何您正在尝试做的数学运算。如果使用正确,它也将比原生 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 装箱/取消装箱,这会带来额外的开销。为了使它更快,必须保持在 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

明显的赢家,麻木

还要注意,创建数据结构所需的时间与求和所需的时间相当,如果不是更多的话。分配内存很慢。

这些人的记忆使用情况:

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位 int 比添加32位 int 更快,所以只有在受到内存/带宽限制的情况下,才值得这样做。

请注意,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

我注意到 typecode Ll快,它也在 IQ中工作。

Python 3.8.5

这是测试代码。 看看这个。

#!/usr/bin/python3
import inspect
from tqdm import tqdm
from array import array




def get_var_name(var):
"""
Gets the name of var. Does it from the out most frame inner-wards.
:param var: variable to get name from.
:return: string
"""
for fi in reversed(inspect.stack()):
names = [var_name for var_name, var_val in fi.frame.f_locals.items() if var_val is var]
if len(names) > 0:
return names[0]


def performtest(func, n, *args, **kwargs):


times = array('f')
times_append = times.append
for i in tqdm(range(n)):
st = time.time()
func(*args, **kwargs)
times_append(time.time() - st)
print(
f"Func {func.__name__} with {[get_var_name(i) for i in args]} run {n} rounds consuming |"
f" Mean: {sum(times)/len(times)}s | Max: {max(times)}s | Min: {min(times)}s"
)


def list_int(start, end, step=1):
return [i for i in range(start, end, step)]


def list_float(start, end, step=1):
return [i + 1e-1 for i in range(start, end, step)]


def array_int(start, end, step=1):
return array("I", range(start, end, step)) # speed I > i, H > h, Q > q, I~=H~=Q


def array_float(start, end, step=1):
return array("f", [i + 1e-1 for i in range(start, end, step)]) # speed f > d




if __name__ == "__main__":


performtest(list_int, 1000, 0, 10000)
performtest(array_int, 1000, 0, 10000)


performtest(list_float, 1000, 0, 10000)
performtest(array_float, 1000, 0, 10000)

结果

测试结果