Python阵列内存占用与列表

bvm*_*ode 6 python arrays list data-structures python-internals

我正在翻阅Fluent Python这本书。它指出,对于所有数字的序列,数组比列表更有效,更快。从我收集到的信息来看,它还减少了内存开销。它指出:“ Python数组和C数组一样精简。”

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

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))
Run Code Online (Sandbox Code Playgroud)

输出

81940352
81528056
Run Code Online (Sandbox Code Playgroud)

ead*_*ead 8

抱歉,但我认为@millimoose 的答案没有很好地解释,当他说数组比列表快时,到底发生了什么或作者的意思。

内存占用:

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

一个 Python-Float 需要超过 8 个字节:

>>> import sys
>>> f=1.0
>>> sys.getsizeof(f)
24
Run Code Online (Sandbox Code Playgroud)

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

>>> class A:
      pass

>>> a=A()
>>> sys.getsizeof(a)
56
Run Code Online (Sandbox Code Playgroud)

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

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

那么为什么在调用sys.getsizeof列表时会看到不同的东西呢?答案是:sys.getsizeof不是递归

sys.getsizeof(object[, 默认])

....

只考虑直接归因于对象的内存消耗,而不是它所引用的对象的内存消耗。

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

>>> lst=[list(range(1000))]
>>> sys.getsizeof(lst)
72
Run Code Online (Sandbox Code Playgroud)

显然,使用了比报告的 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
Run Code Online (Sandbox Code Playgroud)

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

速度:

作者并不是说,array.array与 python-interpreter 一起使用会加快速度。相反,使用array.arraypython-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)
Run Code Online (Sandbox Code Playgroud)

几乎慢了 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)
Run Code Online (Sandbox Code Playgroud)

快了近 10 倍!


mil*_*ose 3

你只是选错了例子。使用的重点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
Run Code Online (Sandbox Code Playgroud)

由于doubles 也是 8 字节宽,因此确实没有比不同的 8 字节更紧凑的方式来存储它们。


至于书中的其余主张,请持保留态度 - 你无法运行 Python 代码 - 也就是说,让 Python 解释器执行操作 - 并且速度与 C 一样快。你仍然会招致将 Python 对象写入数组或从数组中读取它们时的开销,更快的方法是在本机函数中对整个数组执行某种大型操作。