numpy.zeros创建数组首次访问时的内存处理机制相关问题
我近期了解到,通过np.empty或np.zeros创建numpy数组时,由于numpy使用calloc申请数组内存,操作系统不会立刻实际分配对应内存,这一机制已有不少相关讨论。
实际上,操作系统直到你尝试访问内存时才会“真正”完成分配。
因此执行如下代码:
l = np.zeros(2**28)
不会使htop等工具统计的系统已用内存上升,只有当你访问该内存时,例如执行:
np.add(l, 0, out=l)
已用内存才会对应增长。
针对该行为我有两个疑问:
1. 已访问的内存是否会被后台复制?
如果我间隔一段时间分批访问内存块,操作系统为了保证内存连续性,是否会在后台复制numpy数组的内容?
i = 100 f[:i] = 3 while True: ... # 执行其他操作 f[i] = ... # 当已分配内存块后面的地址被其他数据占用时,操作系统是否会重新分配内存并将已填充的数组内容复制到新地址? i = i + 1
2. 仅访问最后一个元素的行为
我原本认为numpy数组的内存是连续的,执行如下代码:
f[-1] = 3
会触发整块内存的分配,无需访问全部内存,但实际htop显示已用内存没有增长到对应数组的大小,请问该现象的原因是什么?
问题1解答
不会发生后台复制。
你调用numpy内存申请接口拿到的是连续的虚拟地址段,这个连续属性在调用calloc完成后就已经确定,和物理内存的分配状态完全无关。操作系统的虚拟内存机制通过页表维护虚拟地址到物理地址的映射,物理内存可以零散分配,不需要和虚拟地址的顺序对应。
你分批访问数组的过程中,操作系统只会给当前访问到的虚拟地址所在的内存页分配对应的物理页,已经完成映射的内存页不会被移动,也不需要复制已有的数组内容,numpy数组的起始指针也不会发生变化,哪怕相邻的物理地址被其他进程占用,也完全不会影响你数组的虚拟地址连续性。
问题2解答
这个现象是由操作系统的分页分配机制导致的,物理内存的分配最小单位是内存页,默认页大小通常为4KB(部分场景下会使用2MB/1GB的大页)。
你提到的numpy数组内存连续,指的是虚拟地址连续,不是物理地址连续。你只访问最后一个元素时,操作系统只会为该元素所在的那一个虚拟页分配物理内存,剩下的未被访问的虚拟地址仍然没有对应的物理页映射,因此htop统计的物理内存占用只会增长1页的大小,不会达到整个数组的容量。只有当你访问到某一页内的任意地址时,才会触发该页的物理内存分配。
内容的提问来源于stack exchange,提问作者mutableVoid

