Python内存管理:关于内存碎片的一些知识

关于这篇文章的一些想法。

最近,我对Python内存管理如何在用于64位Ubuntu的 Python3的CPython中工作感兴趣。

一点理论


glibc系统库具有一个malloc分配器。 每个进程都有一个称为堆的内存区域。 通过调用malloc函数动态分配内存,我们从该进程的堆中获取了一块。 如果请求的内存很小(不超过128KB),则可以从空闲块列表中获取内存。 如果这不可能,那么将使用mmap (sbrk,brk)系统调用来分配内存。 mmap系统调用将虚拟内存映射到物理内存。 内存以4KB页显示。 大块(超过128KB)总是通过mmap系统调用分配的。 释放内存时,如果空闲的小块在未冻结的内存区域上接壤,则部分内存可能会返回操作系统。 大块立即返回到操作系统。

信息来自有关C语言中的分配器演讲。

CPython对于“私有堆”有自己的分配器(PyMalloc),对于在“第一个堆”上工作的每种类型的对象都有分配器。 PyMalloc通过名为Arenas的操作系统库中的malloc请求256KB内存块。 反过来,它们按4KB划分为“池”。 每个池分为固定大小的块,并且每个池可以分为64个大小之一的块。

每种类型的分配器都使用已经分配的块(如果有)。 如果不存在,PyMalloc将从第一个竞技场发布一个新的Pool,在该竞技场中有一个新Pool的位置(竞技场按占用率降序“排序”)。 如果这不起作用,则PyMalloc要求操作系统提供新的Arena。 除非请求的内存大小大于512B,否则将通过malloc从系统库直接分配内存。

删除对象后,内存不会返回到操作系统,而块只是返回到相应的池,而池则返回到Arenas。 当所有块均从其中释放后,竞技场将返回到操作系统。 事实证明,如果Arena中使用的块数量相对较少,那么PVM将使用Arena中所有相同的内存。 但是,由于通过mmap分配了超过128KB的块,因此免费的Arena将立即返回到操作系统。

我想重点谈两点:

  1. 事实证明,PyMalloc在创建新Arena时会分配256KB 物理内存。
  2. 仅免费的Arenas返回操作系统。

例子


考虑以下示例:

iterations = 2000000 l = [] for i in range(iterations): l.append(None) for i in range(iterations): l[i] = {} s = [] # [1] # s = l[::2] # [2] # s = l[2000000 // 2::] # [3] # s = l[::100] # [4] for i in range(iterations): l[i] = None for i in range(iterations): l[i] = {} 

在该示例中,创建了200万个元素的列表l,所有元素都指向一个None对象。 在下一个循环中,将为每个元素创建一个对象-空字典。 然后,创建第二个列表s,其第二个元素指向由第一个列表的某些元素引用的某些对象。 在下一次爬网之后,列表l中的项目再次开始指向无对象。 在最后一个循环中,再次为第一个列表中的每个元素创建字典。

S列表选项:

  1.  s = [] 
  2.  s = l[::2] 
  3.  s = l[200000 // 2::] 
  4.  s = l[::100] 

我们对每种情况下的内存消耗都很感兴趣。

我们将在启用PyMalloc日志记录的情况下运行此脚本:

 export PYTHONMALLOCSTATS="True" && python3 source.py 2>result.txt 

结果说明


在图片中,您可以看到每种情况下的内存消耗。 在横坐标轴上,值与消耗的时间没有相关性,只有日志中的每个值都与其序列号相关联。

图片

“没有元素”


在第一种情况下,列表s为空。 在第二个周期中创建对象之后,将消耗大约500MB的内存。 并且在第三个周期中删除所有这些对象,并将使用的内存返回给操作系统。 在最后一个周期中,将再次分配对象的内存,这将导致相同的500MB的消耗。

“每秒”


在使用列表l的每个第二个元素创建一个列表的情况下,我们可以注意到内存没有返回给操作系统。 也就是说,在这种情况下,我们观察到以下情况:删除了大约250MB的字典,但是在每个池中都有未删除的块,因此不会释放相应的Arenas。 但是,当我们第二次创建字典时,这些池中的空闲块将被重用,这就是为什么只分配了约250MB新内存的原因。

“下半场”


在我们从列表l的后半部分创建列表的情况下,前半部分位于单独的Arenas中,因此大约有250 MB的内存返回给操作系统。 之后,大约500MB被重新分配给新词典,这就是为什么总消耗量为750MB的原因。

在这种情况下,与第二种情况不同,内存部分返回了操作系统。 一方面,它允许其他进程使用此内存,另一方面,需要系统调用来释放并重新分配它。

“每一百”


最后一种情况似乎是最有趣的。 在此,我们从第一个列表的每100个元素中创建第二个列表,这大约需要5MB。 但是由于每个竞技场中仍有一定数量的已占用块,因此无法释放此内存,并且消耗量保持在500MB的水平。 当我们第二次创建字典时,几乎没有新的内存被分配,而那些第一次分配的块将被重用。

在这种情况下,由于内存碎片,我们使用的内存是所需数量的100倍以上。 但是,当重复需要此内存时,我们不必进行系统调用即可分配它。

总结


值得注意的是,使用许多分配器时可能会发生内存碎片。 但是,您需要谨慎使用某些数据结构,例如具有树结构的数据结构,例如搜索树。 由于添加和删除的任意操作都可能导致上述情况,因此,就内存消耗而言,使用这些结构的明智性将受到质疑。

渲染图像的代码
 def parse_result(filename): ms = [] with open(filename, "r") as f: for line in f: if line.startswith("Total"): m = float(line.split()[-1].replace(",", "")) / 1024 / 1024 ms.append(m) return ms ms_1 = parse_result("_1.txt") ms_2 = parse_result("_2.txt") ms_3 = parse_result("_3.txt") ms_4 = parse_result("_4.txt") import matplotlib.pyplot as plt plt.figure(figsize=(20, 15)) fontdict = { "fontsize": 20, "fontweight" : 1, } plt.subplot(2, 2, 1) plt.title(" ", fontdict=fontdict, loc="left") plt.plot(ms_1) plt.grid(b=True, which='major', color='#666666', linestyle='-.') plt.minorticks_on() plt.grid(b=True, which='minor', color='#999999', linestyle='-', alpha=0.2) plt.tick_params(axis='both', which='major', labelsize=15, labelbottom=False) plt.ylabel("MB", fontsize=15) plt.subplot(2, 2, 2) plt.title(" ", fontdict=fontdict, loc="left") plt.plot(ms_2) plt.grid(b=True, which='major', color='#666666', linestyle='-.') plt.minorticks_on() plt.grid(b=True, which='minor', color='#999999', linestyle='-', alpha=0.2) plt.tick_params(axis='both', which='major', labelsize=15, labelbottom=False) plt.ylabel("MB", fontsize=15) plt.subplot(2, 2, 3) plt.title(" ", fontdict=fontdict, loc="left") plt.plot(ms_3) plt.grid(b=True, which='major', color='#666666', linestyle='-.') plt.minorticks_on() plt.grid(b=True, which='minor', color='#999999', linestyle='-', alpha=0.2) plt.tick_params(axis='both', which='major', labelsize=15, labelbottom=False) plt.ylabel("MB", fontsize=15) plt.subplot(2, 2, 4) plt.title(" ", fontdict=fontdict, loc="left") plt.plot(ms_4) plt.grid(b=True, which='major', color='#666666', linestyle='-.') plt.minorticks_on() plt.grid(b=True, which='minor', color='#999999', linestyle='-', alpha=0.2) plt.tick_params(axis='both', which='major', labelsize=15, labelbottom=False) plt.ylabel("MB", fontsize=15) 

Source: https://habr.com/ru/post/zh-CN479744/


All Articles