【发布时间】:2021-07-02 13:13:30
【问题描述】:
背景/基本原理
我有一个 Python 软件,它从测量仪器中获取基于事件的数据,对其进行处理并将结果写入磁盘。输入事件使用相当多的内存,接近 10MB/事件。当输入事件率很高时,处理可能不够快,导致事件堆积在内部队列中。这种情况一直持续到可用内存几乎用完,此时它会指示仪器限制采集速率(这有效,但会降低准确性)。通过使用psutil.virtual_memory().available 观察可用的系统内存来检测这一时刻。为了获得最佳结果,一旦从已处理的事件中获得足够的内存,就应该禁用限制。这就是问题所在。
看起来,CPython 解释器不(或不总是)将释放的内存返回给操作系统,这使得psutil(以及gnome-system-monitor)报告可用内存不足。但是,内存实际上是可用的,因为手动禁用限制将再次填满队列,而不会进一步增加消耗,除非将更多的事件放入队列中。
以下示例可能会显示此行为。在我的计算机上,可能有 50% 的调用显示了问题,而其余的则正确释放了内存。有几次内存在迭代 0 结束时被释放,但不是在迭代 1 和 2 结束时释放,所以行为似乎有点随机。
#!/usr/bin/env python3
import psutil
import time
import queue
import numpy as np
def get_avail() -> int:
avail = psutil.virtual_memory().available
print(f'Available memory: {avail/2**30:.2f} GiB')
return avail
q: 'queue.SimpleQueue[np.ndarray]' = queue.SimpleQueue()
for i in range(3):
print('Iteration', i)
# Allocate data for 90% of available memory.
for i_mat in range(round(0.9 * get_avail() / 2**24)):
q.put(np.ones((2**24,), dtype=np.uint8))
# Show remaining memory.
get_avail()
time.sleep(5)
# The data is now processed, releasing the memory.
try:
n = 0
while True:
n += q.get_nowait().max()
except queue.Empty:
pass
print('Result:', n)
# Show remaining memory.
get_avail()
print(f'Iteration {i} ends')
time.sleep(5)
print('Program done.')
get_avail()
预期的行为将是在打印结果之前可用内存不足,而打印后可用内存很高:
Iteration 0
Available memory: 22.24 GiB
Available memory: 2.17 GiB
Result: 1281
Available memory: 22.22 GiB
Iteration 0 ends
但是,它也可能以这样的方式结束:
Iteration 1
Available memory: 22.22 GiB
Available memory: 2.19 GiB
Result: 1280
Available memory: 2.36 GiB
Iteration 1 ends
集成对垃圾收集器的显式调用,例如
print('Result:', n)
# Show remaining memory.
get_avail()
gc.collect(0)
gc.collect(1)
gc.collect(2)
get_avail()
print(f'Iteration {i} ends')
没有帮助,内存可能仍在使用中。
我知道会有一些解决方法,例如检查队列大小而不是可用内存。但这会使系统更容易耗尽资源,以防其他进程碰巧消耗大量内存。使用multiprocessing 并不能解决问题,因为事件提取必须单线程完成,因此提取过程在其队列中总是会遇到同样的问题。
问题
-
如何查询解释器的内存管理,以了解引用的对象使用了多少内存,以及有多少只是保留供将来使用而不返回给操作系统?
-
如何强制解释器将保留的内存返还给操作系统,以便报告的可用内存实际增加?
目标平台是 Ubuntu 20.04+ 和 CPython 3.8+,不需要支持以前的版本或其他版本。
谢谢。
【问题讨论】:
-
我认为您无法将内存“归还”给操作系统。一些完全未使用的页面可能会被调出,但除此之外,您会陷入困境,无法终止进程并开始新的进程。
-
明确一点,垃圾收集器只需要打破引用循环;否则,Python 内存分配器可以回收的任何内存都是根据引用计数达到零的时间来完成的。不过,无论哪种方式,CPython 中的任何内容都不会将内存释放回操作系统。
-
为什么要根据操作系统的可用内存来节流?如果 Python 仍然持有该内存,它将尽可能重用它,而不是向操作系统请求更多。
-
@Tom Karzes:确实有一些参考资料表明解释器永远不会将内存还给操作系统。但示例代码表明它可能会发生,但并非总是如此。
-
@chepner:需要限制以避免进程因内存不足而被终止。可用内存量用于决定何时进行节流,理想情况下,还用于决定何时返回全速采集。解释器内存当然会被重用,但是软件不知道这个保留的内存有多少是可用的。
标签: python python-3.x memory