关于这篇文章的一些想法。最近,我对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将立即返回到操作系统。
我想重点谈两点:
- 事实证明,PyMalloc在创建新Arena时会分配256KB 物理内存。
- 仅免费的Arenas返回操作系统。
例子
考虑以下示例:
iterations = 2000000 l = [] for i in range(iterations): l.append(None) for i in range(iterations): l[i] = {} s = []
在该示例中,创建了200万个元素的列表l,所有元素都指向一个None对象。 在下一个循环中,将为每个元素创建一个对象-空字典。 然后,创建第二个列表s,其第二个元素指向由第一个列表的某些元素引用的某些对象。 在下一次爬网之后,列表l中的项目再次开始指向无对象。 在最后一个循环中,再次为第一个列表中的每个元素创建字典。
S列表选项:
s = []
s = l[::2]
s = l[200000 // 2::]
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)