Mmap()写入时复制技巧:减少数组副本的内存使用量

2020-09-18 14:12:49

假设您有一个阵列,您需要创建一些副本并修改这些副本。通常,内存使用量会随着副本的数量而增加:如果您的原始阵列是1 GB的RAM,那么每个副本将占用1 GB的RAM。

但通常情况下,您只更改了阵列的一小部分,理想情况下,内存开销只是您更改的那部分副本。

事实证明,有一个操作系统工具可以实现这一点:mmap()的写入时复制功能。

如果要修改数组的副本,通常的方法是分配更多内存,并将原始数组的内容复制到新的内存块中。例如:

>;>;>;导入numpy,psutil>;>;>;def memory_use():...。CURRENT_PROCESS=psutil。进程()...。Memory=CURRENT_PROCESS。MEMORY_INFO()。RSS..。Print(int(memory/(1024*1024)),";MB";)...>;>;>;array1=numpy。1((1024,1024,50))>;>;>;memory_use()428MB>;>;array2=array1。Copy()>;>;>;Memory_Usage()827 MB。

页面是4KB的区块,是操作系统的内存管理单位。

在理想情况下,第二个数组将只存储与第一个数组不同之处:只要差别很小,额外的内存使用量就会很小。这就是mmap()的写入时复制功能的用武之地(或者Windows上的等效API;NumPy将两者都包装起来)。

如果您不熟悉mmap(),请参阅我将mmap()与HDF5和Zarr进行比较的概述。

要在这种模式下使用mmap(),我们需要一个备份文件。虽然涉及到一个文件,但只要有足够的内存可用,该文件几乎就是一个实现细节;它需要存在,但不会对性能产生太大影响。

注意:在Linux上,您可以更进一步,使用memfd_create API创建内存中的文件,该API可以在Python3.8及更高版本中使用,方法是执行os.fdopen(os.memfd_create(";mymemfile";),";rb+&34;),然后将文件“截断”为合适的大小。

Numpy.lib.format.open_memmap()函数将打开一个适当大小的文件;我们将从创建初始数组开始:

>;>;>;del array1,array2>;>;>;memory_use()20 MB>;>;open_memmap=numpy。利布。格式化。Open_memmap>;>;mmap_array1=open_memmap(";/tmp/myarray";,mode=";w+";,form=(1024,1024,50)>;>;memory_use()22 MB>;>;>;mmap_array1[:]=1>;>;>;mmap_array1[0]=10>;>;MEMORY_USAGE()422 MB。

最初数组只是零(至少在Linux和MacOS上是这样;Windows可能不同),所以操作系统足够聪明,不会分配任何新内存。一旦我们设置了一些值,内存使用量就会相应增加。

接下来,让我们创建一个副本:我们将使用mode=";c";mmap()同一个文件,这意味着写入时复制。在Linux或MacOS等Unix系统上,这将转换为mmap()API的MAP_PRIVATE标志。

>;>;>;mmap_array2=open_memmap(";/tmp/myarray";,mode=";c";,form=(1024,1024,50)>;>;>;mmap_array2[0,0,0]10.0>;>;>;mmap_array2[10,0,1]1.0>;>;Memory_Usage()422 MB。

现在我们有了阵列的另一个副本,具有相同的内容…。但是内存使用没有改变!

现在让我们修改第二个数组,我们将看到内存使用率是如何增加的,但原始数组没有变化。

只存储副本中与原始副本不同的部分,这样可以节省内存。

当我们使用MAP_PRIVATE标志mmap()文件时,每个手册页会发生以下情况:

MAP_PRIVATE创建私有写入时拷贝映射。映射的更新对映射同一文件的其他进程不可见,并且不会传递到基础文件。未指定在调用thmap()之后对文件所做的更改是否在映射区域中可见。

请注意,对文件所做的更改可能是可见的,也可能是不可见的,该行为是未指定的。因此,最好不要修改原始数组。

回到我们的目标,我们通过使用写入时复制来节省内存。这意味着第二个数组中的页指向第一个数组,直到对它们进行了一些更改。只有当您写入该页时,才会创建副本并应用写入。

然后,我们对第二个阵列的一部分进行了一些更改。这些被修改的页面被复制,然后被修改-其余的仍然指向原始阵列。例如,如果我们修改了阵列内存表示中的前4096个字节中的一些数据,则会分配一个新页面,该页面是第一个阵列中的页面的副本:

在这种情况下,写入时复制只为实际更改的数据分配内存,从而节省内存。只需确保不修改原始数组;根据操作系统的不同,您可能会产生意想不到的结果。

对于其他数据结构,比如字典或列表,您可以使用不可变的数据结构来减少大部分相似副本的内存使用;在Python中,pyrsistent库是一种实现。

了解更多减少内存使用的技术-阅读Python的小型大数据指南的其余部分。

您的Python批处理使用了太多内存,并且您不知道代码的哪一部分负责。

您需要一个工具来准确地告诉您应该将优化工作集中在哪里,这是一个为数据科学家和科学家设计的工具。了解FIL内存分析器如何帮助您。

获取免费的小抄,总结如何使用Python、NumPy和Pandas在有限内存的情况下处理大量数据。

此外,大约每周您都会收到新的文章,向您展示如何处理大量数据,并更全面地提高您的软件工程技能,从测试到打包再到性能: