array.array()如何使用这么小的内存空间?

时间:2018-08-16 16:53:31

标签: python arrays memory-management python-internals

我不确定array.array()类为什么使用的内存少于sys.getsizeof报告的内存:

from array import array
a = array('f')
for i in range(500000):
    a.append(float(i))
sys.getsizeof(a)
# 2100228
sum(sys.getsizeof(i) for i in a)
# 12000000 (makes sense, 24 bytes * 500K)
# 2100228 + 12000000 = 14100228
# 14100228 / 1000 = 14,100.228KB
# 14,100.228 / 1000 = 14.1MB

但是,在任务管理器中检查进程,程序内存仅增加3MB。那么,如何仅使用3MB的空间却占用对象14.1MB的进程呢?

3 个答案:

答案 0 :(得分:4)

Python float是功能齐全的对象,它知道其类型(因此具有方法)并且可以被垃圾回收等等。在CPython(您可能正在使用的Python实现)中,此方法通过存储指向类型对象的指针(8个字节)和引用计数(另外8个字节)以及实际的IEEE float64值(另外8个字节)来实现,因此它至少有24个字节长。

list仅存储对Python对象的引用。因此,{50}浮动的list列表本身将占用4MB以上的空间(存储所有这些引用),而所有引用的float对象将总共再占用12MB。

array.array不存储float对象,它仅存储IEEE float64值的位(8个字节),然后在您每次运行时动态创建这些float对象用arr[0]请求一个。这使它变小了很多(整个东西只占用4MB的内存),但速度也较慢。 1

当然,您甚至都没有存储IEEE float64数组(即d,而不是f),而是存储float32。其中的五百万需要2MB。

如果您希望两全其美,那么第三方库NumPy可以像array.array一样存储这些位,并且可以对这些位进行计算而不必创建和销毁{{1} }到处都是对象,因此都更小。


因此,当您请求500K浮点数组的大小时,float为2MB,因为它仅存储500K本机IEEE float32值(加上几十个固定开销字节)。

但是,当您遍历该数组并计算每个成员的大小时,实际上是在动态创建24字节f对象。所有这些临时对象的总大小为12MB。但是它们是临时的,一旦您检查了每一个的大小,便会忘记它,它会变成垃圾并被清除,并且相同的24个字节可用于下一个重复使用。


关于任务管理器为何显示内存增加3MB的原因:

几乎每个程序都通过以下方式工作:拥有一堆内存,从该堆中分配出来,并且仅在需要更多内存时才要求操作系统大块地提供更多内存。 (CPython通过在基本堆之上添加两个自定义堆,使这一过程变得更加复杂,但是不必担心。)

因此,假设解释器的堆中剩余2MB的空间,而您要求它分配4MB的对象。它需要回到Windows,并要求至少增加2MB的内存。它得到的超出了需要的一点(因此它不会立即需要返回并要求更多),结果约为3MB。当然,这只是最终从操作系统中获得3MB的多种方式中的一种,要弄清到底是哪一种发生,需要进行复杂的调试(比做更多有用的事情要复杂得多,例如仅跟踪程序的实际堆使用情况)。

如您所见,这使任务管理器测量内存使用情况变得毫无用处,但笔触非常宽泛。 (而且实际上甚至更糟糕的是,一旦您遇到以下问题,例如Python何时将可用内存返回给Windows,内存碎片化,操作系统是否超量使用,何时可以在虚拟内存中重新映射页面以及何时无法重新映射页面等等。各种其他复杂性。)


1。尽管它并不总是较慢。有时,更紧凑的内存可以为您提供缓存或虚拟内存方面的巨大优势,以弥补浪费的时间来创建和销毁整个对象。

答案 1 :(得分:3)

您的a数组实际上不包含for i in a产生的任何对象。这些对象在访问时生成。 a包含原始的32位浮点数,而不是浮点对象。

答案 2 :(得分:0)

docs所述,“ array模块定义了一个对象类型,该对象类型可以紧凑地表示基本值的数组:字符,整数,浮点数” 。也就是说,a[i]将需要存储类型信息,而对于整个a数组,则只需要存储一次。