array.array() 如何使用这么小的内存 space?
How does array.array() use such little memory space?
我不确定为什么 array.array()
class 使用的内存比 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呢?
如docs、"the array
module defines an object type which can compactly represent an array of basic values: characters, integers, floating point numbers"中所写。也就是说 a[i]
需要存储类型信息,而对于整个 a
数组你只需要存储一次。
您的 a
数组实际上不包含 for i in a
生成的任何对象。这些对象是在访问时生成的。 a
包含原始 32 位浮点数,而不是浮点数对象。
A Python float
是一个功能齐全的对象,它知道它的类型(所以它有方法)并且可以被垃圾收集等等。在 CPython(您可能正在使用的 Python 实现)中,这通过存储指向类型对象的指针(8 个字节)和一个引用计数(另外 8 个字节)以及实际的 IEEE float64 值(多 8 个字节),因此它至少有 24 个字节长。
A list
仅存储对 Python 对象的引用。因此,50 万个浮点数的 list
将占用列表本身的 4MB 多一点(存储所有这些引用),加上所有引用的 float
对象总共将占用另外 12MB。
array.array
不存储 float
对象,它只存储 IEEE float64 值的位(8 字节),然后动态创建那些 float
对象每当你要求一个,例如 arr[0]
。这使得它变得更小——整个东西只需要 4MB 的内存——但也更慢。1
当然,您甚至没有存储 IEEE float64 数组(即 d
,而不是 f
),而是 float32。其中 50 万占用 2MB。
如果你想要两全其美,第三方库 NumPy 可以像 array.array
一样存储位,并且它可以对这些位进行计算而无需创建和销毁 float
对象到处都是,所以它更小 并且 更快。
因此,当您要求 500K f
浮点数数组的大小时,这是 2MB,因为它仅存储 500K 本机 IEEE float32 值(加上几十个字节的固定开销)。
但是当您遍历该数组并计算每个成员的大小时,您实际上是在动态创建 24 字节的 float
对象。所有这些临时对象的总大小为 12MB。但它们是临时的——一旦你检查了每个的大小,你就会忘记它,它变成垃圾并被清理,同样的 24 个字节可以被下一个重新使用。
至于为什么任务管理器显示内存增加了 3MB:
几乎每个程序的工作方式都是拥有一堆内存,从该堆中分配内存,并且仅在需要更多时才向 OS 请求大块内存。 (CPython 在基本堆之上有两个自定义堆,这让事情变得更加复杂,但别担心。)
所以,假设解释器在其堆中剩余 2MB space,而您要求它分配一个 4MB 的对象。它需要返回 Windows 并请求至少 2MB 的内存。它得到的比它需要的多一点(所以它不需要立即返回并要求更多),结果大约是 3MB。当然,这只是您最终可以从 OS 中获得 3MB 的众多方法中的一种,并且准确地弄清楚发生了什么需要复杂的调试(比做更有用的事情更复杂,比如跟踪堆的实际使用情况你的程序)。
如您所见,这使得任务管理器测量内存使用情况变得毫无用处,除了非常粗略的笔划。 (而且实际上比这更糟糕,一旦你遇到诸如何时 Python returns 将内存释放到 Windows、内存碎片化时会发生什么、OS 是否过度使用等问题,何时可以和不能在虚拟内存中重新映射页面,以及各种其他复杂情况。)
1.虽然它并不总是更慢。有时在内存中更紧凑会给您在缓存或虚拟内存方面带来很多优势,这足以弥补到处创建和销毁对象所浪费的时间。
我不确定为什么 array.array()
class 使用的内存比 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呢?
如docs、"the array
module defines an object type which can compactly represent an array of basic values: characters, integers, floating point numbers"中所写。也就是说 a[i]
需要存储类型信息,而对于整个 a
数组你只需要存储一次。
您的 a
数组实际上不包含 for i in a
生成的任何对象。这些对象是在访问时生成的。 a
包含原始 32 位浮点数,而不是浮点数对象。
A Python float
是一个功能齐全的对象,它知道它的类型(所以它有方法)并且可以被垃圾收集等等。在 CPython(您可能正在使用的 Python 实现)中,这通过存储指向类型对象的指针(8 个字节)和一个引用计数(另外 8 个字节)以及实际的 IEEE float64 值(多 8 个字节),因此它至少有 24 个字节长。
A list
仅存储对 Python 对象的引用。因此,50 万个浮点数的 list
将占用列表本身的 4MB 多一点(存储所有这些引用),加上所有引用的 float
对象总共将占用另外 12MB。
array.array
不存储 float
对象,它只存储 IEEE float64 值的位(8 字节),然后动态创建那些 float
对象每当你要求一个,例如 arr[0]
。这使得它变得更小——整个东西只需要 4MB 的内存——但也更慢。1
当然,您甚至没有存储 IEEE float64 数组(即 d
,而不是 f
),而是 float32。其中 50 万占用 2MB。
如果你想要两全其美,第三方库 NumPy 可以像 array.array
一样存储位,并且它可以对这些位进行计算而无需创建和销毁 float
对象到处都是,所以它更小 并且 更快。
因此,当您要求 500K f
浮点数数组的大小时,这是 2MB,因为它仅存储 500K 本机 IEEE float32 值(加上几十个字节的固定开销)。
但是当您遍历该数组并计算每个成员的大小时,您实际上是在动态创建 24 字节的 float
对象。所有这些临时对象的总大小为 12MB。但它们是临时的——一旦你检查了每个的大小,你就会忘记它,它变成垃圾并被清理,同样的 24 个字节可以被下一个重新使用。
至于为什么任务管理器显示内存增加了 3MB:
几乎每个程序的工作方式都是拥有一堆内存,从该堆中分配内存,并且仅在需要更多时才向 OS 请求大块内存。 (CPython 在基本堆之上有两个自定义堆,这让事情变得更加复杂,但别担心。)
所以,假设解释器在其堆中剩余 2MB space,而您要求它分配一个 4MB 的对象。它需要返回 Windows 并请求至少 2MB 的内存。它得到的比它需要的多一点(所以它不需要立即返回并要求更多),结果大约是 3MB。当然,这只是您最终可以从 OS 中获得 3MB 的众多方法中的一种,并且准确地弄清楚发生了什么需要复杂的调试(比做更有用的事情更复杂,比如跟踪堆的实际使用情况你的程序)。
如您所见,这使得任务管理器测量内存使用情况变得毫无用处,除了非常粗略的笔划。 (而且实际上比这更糟糕,一旦你遇到诸如何时 Python returns 将内存释放到 Windows、内存碎片化时会发生什么、OS 是否过度使用等问题,何时可以和不能在虚拟内存中重新映射页面,以及各种其他复杂情况。)
1.虽然它并不总是更慢。有时在内存中更紧凑会给您在缓存或虚拟内存方面带来很多优势,这足以弥补到处创建和销毁对象所浪费的时间。