Python 数组内存占用与列表

Python Array Memory Footprint versus List

正在翻《Fluent》这本书Python。它指出,对于所有数字的序列,数组比列表更高效、更快速。从我收集到的信息来看,它的内存开销也更少。它表示 "A Python array is as lean as a C array."

我很好奇为什么数组在这里显示的内存比列表多。

import array
from random import random
import sys

floats = array.array('d', (random() for i in range(10**7)))
L = [random() for i in range(10**7)]
print(sys.getsizeof(floats))
print(sys.getsizeof(L))

输出

81940352
81528056

你刚刚选错了例子。使用 array 的要点是当您需要存储其本机表示小于 Python 对象引用的项目时。 (这里似乎是 8 个字节。)例如如果你这样做:

from array import array
from os import urandom
a = array('B', urandom(1024))
l = list(a)
sys.getsizeof(a) # => 1155
sys.getsizeof(l) # => 9328

因为 double 也是 8 字节宽,所以没有比不同的 8 字节更紧凑的存储方式了。


至于本书中的其他主张,请对它们持保留态度 - 您不能 运行 Python 代码 - 也就是说,让操作由 Python 解释器 - 并且和 C 一样快。在将 Python 对象写入数组或从数组中读取它们时,您仍然会产生开销,更快的方法是对整个数组进行某种大操作本机函数。

抱歉,但我认为 @millimoose 的回答没有很好地解释真正发生的事情或作者的意思,当他说数组比列表快时。

内存占用:

double 需要 8 个字节,如果将其存储在 array 中,这正是需要多少内存 - 它不是存储为 Python-Float,而是存储为原始 8 -字节值。然而,由于过度分配和数组对象中保存的一些额外数据(数组大小、缓冲区大小、数组中值的类型等),开销很小。

一个Python-Float需要8个字节以上:

>>> import sys
>>> f=1.0
>>> sys.getsizeof(f)
24

24 字节 - 对于 Python 对象来说非常小!例如,一个通常的空 Python-object 需要 (Python3):

>>> class A:
      pass

>>> a=A()
>>> sys.getsizeof(a)
56

56 字节。有一些技巧可以减少所需的字节数,它们都用于 Python-Floats 但你仍然需要 8 个字节用于双精度值,另外 8 个字节用于引用计数器以及 8 个字节用于指向的指针类型描述(所以对象知道它是一个 Float 对象)。

此外,列表中存储的不是对象本身,而是指向它的引用(即指针),它本身需要 8 个字节。因此,在列表中保存 Python-float 基本上需要 32 个字节——是所用内存量的 4 倍。

那么,为什么在为列表调用 sys.getsizeof 时会看到不同的东西?答案:sys.getsizeofnot recursive:

sys.getsizeof(object[, default])

....

Only the memory consumption directly attributed to the object is accounted for, not the memory consumption of objects it refers to.

这意味着列表的 getsizeof 只计算引用浮点对象所需的内存(每个引用 8 个字节),而不计算对象的大小。为了说明这一点:

>>> lst=[list(range(1000))]
>>> sys.getsizeof(lst)
72

显然,使用的内存比报告的 72 字节多。

要查看实际内存消耗,您需要考虑解释器所需的内存:

>>> /usr/bin/time -fpeak_used_memory:%M python -c "l=list((float(i) for i in range(10**7)))"
peak_used_memory:326832
>>> /usr/bin/time -fpeak_used_memory:%M python -c "import array; a=array.array('d',(float(i) for i in range(10**7)))"
peak_used_memory:88076

正如我们所见,差异(320 MB 与 80MB)大约是预期因子 4。

速度:

作者并不是说使用 array.array 和 python-interpreter 会给你一个加速。相反,使用 array.array 和 python-operations 会使它变慢,因为首先必须将原始 double 值转换为 Python-Floats:

lst=list(range(10**6))
%timeit sum(lst)
7.19 ms ± 461 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

import array
a=array.array('i',range(10**6))
%timeit sum(a)
17.9 ms ± 43.7 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

几乎慢了 3 倍!

但是,有可能加快速度 - 只是不那么简单。为此,可以使用 numpy、cython 或 numba。例如:

import numpy as np
b=np.array(range(10**6), dtype=np.int32)
%timeit b.sum()
1.07 ms ± 24.8 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

几乎快了 10 倍!