Python3:将未使用的解释器内存还给 OS
Python3: Give unused interpreter memory back to the OS
背景/理由
我有一个 Python 软件,它从测量仪器中获取基于事件的数据,对其进行处理并将结果写入磁盘。输入事件使用相当多的内存,大约 10MB/事件。当输入事件率很高时,处理速度可能不够快,导致事件堆积在内部队列中。这一直持续到可用内存几乎用完,这时它会指示仪器限制采集速率(这有效,但会降低准确性)。这个时刻是通过使用 psutil.virtual_memory().available
查看可用的系统内存来检测的。为获得最佳结果,一旦处理的事件有足够的内存可用,就应禁用限制。这就是麻烦所在。
看起来,CPython 解释器没有(或不总是)return 释放内存给 OS,这使得 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
无法解决问题,因为事件提取必须单线程完成,因此提取进程在其队列中总是会遇到同样的问题。
问题
我如何查询解释器的内存管理以查明引用对象使用了多少内存,以及仅保留供将来使用而不还给 OS 的内存?
如何强制解释器将保留的内存还给 OS,以便报告的可用内存实际增加?
目标平台是Ubuntu 20.04+和CPython 3.8+,不需要支持以前的版本或其他风格。
谢谢。
它不是 Python 或 NumPy
正如问题的评论中已经指出的那样,观察到的效果并非特定于 Python(或 NumPy,因为内存实际上用于大型 ndarray)。相反,它是使用的 C 运行time 的一个特性,在本例中是 glibc。
堆和内存映射
当使用 malloc() (as done by NumPy when an array is allocated), the runtime decides if it should use the brk syscall for smaller chunks or mmap 为更大的块请求内存时。 sbrk
用于增加堆大小。分配的堆 space 可能会返回给 OS,但前提是堆顶部有足够的连续 space。这意味着恰好位于堆顶端的对象已经有几个字节可能会有效地阻止进程将任何堆内存返回给 OS。内存没有被浪费,因为 运行 时间将使用堆上其他已释放的 space 用于后续调用 malloc()
,但进程仍使用内存,因此从未报告为在进程终止之前可用。
通过 mmap()
分配内存页面效率较低,但它的好处是这些页面可以在不再需要时返回给 OS。性能下降是因为每当内存页面被映射或取消映射时,内核都会参与;特别是因为内核出于安全原因必须将映射页面清零。
mmap 阈值
malloc()
使用请求的内存量的阈值来决定是否应该使用堆或 mmap()
。这个阈值在最新版本的 glibc 中是动态的,但可以使用 mallopt() 函数更改:
M_MMAP_THRESHOLD
[...]
Note: Nowadays, glibc uses a dynamic mmap threshold by default. The initial value of the threshold is 128*1024, but when blocks larger than the current threshold and less than or equal to DEFAULT_MMAP_THRESHOLD_MAX are freed, the threshold is adjusted upwards to the size of the freed block. When dynamic mmap thresholding is in effect, the threshold for trimming the heap is also dynamically adjusted to be twice the dynamic mmap threshold. Dynamic adjustment of the mmap threshold is disabled if any of the M_TRIM_THRESHOLD, M_TOP_PAD, M_MMAP_THRESHOLD, or M_MMAP_MAX parameters is set.
至少可以通过两种方式调整阈值:
正在调用 mallopt()
。
通过设置环境变量MALLOC_MMAP_THRESHOLD_
(注意结尾的下划线)。
应用于示例代码
该示例以 2**24
字节或 16MiB 的块为单位分配(和释放)内存。根据该理论,固定的 MMAP_THRESHOLD
稍微低于此值应该确保所有大数组都使用 mmap()
分配,允许它们被取消映射并返回给 OS.
第一个运行没有修改:
$ ./test_mem.py
Iteration 0
Available memory: 21.45 GiB
Available memory: 2.17 GiB
Result: 1235
Available memory: 21.50 GiB
Iteration 0 ends
Iteration 1
Available memory: 21.50 GiB
Available memory: 2.13 GiB
Result: 1238
Available memory: 3.95 GiB
Iteration 1 ends
Iteration 2
Available memory: 4.02 GiB
Available memory: 4.02 GiB
Result: 232
Available memory: 4.02 GiB
Iteration 2 ends
Program done.
Available memory: 4.02 GiB
迭代 1 和 2 中未返回内存。
现在让我们设置 1MiB 的固定阈值:
$ MALLOC_MMAP_THRESHOLD_=1048576 ./test_mem.py
Iteration 0
Available memory: 21.55 GiB
Available memory: 2.13 GiB
Result: 1241
Available memory: 21.52 GiB
Iteration 0 ends
Iteration 1
Available memory: 21.52 GiB
Available memory: 2.11 GiB
Result: 1240
Available memory: 21.52 GiB
Iteration 1 ends
Iteration 2
Available memory: 21.51 GiB
Available memory: 2.12 GiB
Result: 1239
Available memory: 21.53 GiB
Iteration 2 ends
Program done.
Available memory: 21.53 GiB
可以看出,在所有三个迭代中,内存都成功返回给 OS。
作为替代方案,也可以通过使用 ctypes 模块调用 mallopt()
将设置集成到 Python 脚本中:
#!/usr/bin/env python3
import ctypes
import psutil
import time
import queue
import numpy as np
libc = ctypes.cdll.LoadLibrary("libc.so.6")
M_MMAP_THRESHOLD = -3
# Set malloc mmap threshold.
libc.mallopt(M_MMAP_THRESHOLD, 2**20)
# ...
免责声明:这些 solutions/workarounds 与平台无关,因为它们使用特定的 glibc 功能。
备注
以上文字主要回答了比较重要的第二个问题“如何强制解释器将保留内存还给OS,从而使报告的可用内存实际增加?”。至于第一个问题“我如何查询解释器的内存管理以找出引用对象使用了多少内存,以及仅保留供将来使用而不还给 OS 的内存量?”,我是无法找到满意的答案。 malloc_stats()
:
libc = ctypes.cdll.LoadLibrary("libc.so.6")
# ... script here ...
libc.malloc_stats()
给出了一些数字,但是这些结果:
Arena 0:
system bytes = 1632264192
in use bytes = 4629984
Total (incl. mmap):
system bytes = 1632858112
in use bytes = 5223904
max mmap regions = 1236
max mmap bytes = 20725514240
对于脚本 运行 不更改 mmap 阈值 我觉得有点困惑。 5MiB 可能是脚本结束时实际使用的内存,但是“系统字节”呢?该进程此时仍使用近 20GiB,因此指示的 1.6GiB 根本不适合图片。
另见
-
Reduce memory fragmentation with MALLOC_MMAP_THRESHOLD_ and MALLOC_MMAP_MAX_
-
Releasing memory in Python
-
-
背景/理由
我有一个 Python 软件,它从测量仪器中获取基于事件的数据,对其进行处理并将结果写入磁盘。输入事件使用相当多的内存,大约 10MB/事件。当输入事件率很高时,处理速度可能不够快,导致事件堆积在内部队列中。这一直持续到可用内存几乎用完,这时它会指示仪器限制采集速率(这有效,但会降低准确性)。这个时刻是通过使用 psutil.virtual_memory().available
查看可用的系统内存来检测的。为获得最佳结果,一旦处理的事件有足够的内存可用,就应禁用限制。这就是麻烦所在。
看起来,CPython 解释器没有(或不总是)return 释放内存给 OS,这使得 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
无法解决问题,因为事件提取必须单线程完成,因此提取进程在其队列中总是会遇到同样的问题。
问题
我如何查询解释器的内存管理以查明引用对象使用了多少内存,以及仅保留供将来使用而不还给 OS 的内存?
如何强制解释器将保留的内存还给 OS,以便报告的可用内存实际增加?
目标平台是Ubuntu 20.04+和CPython 3.8+,不需要支持以前的版本或其他风格。
谢谢。
它不是 Python 或 NumPy
正如问题的评论中已经指出的那样,观察到的效果并非特定于 Python(或 NumPy,因为内存实际上用于大型 ndarray)。相反,它是使用的 C 运行time 的一个特性,在本例中是 glibc。
堆和内存映射
当使用 malloc() (as done by NumPy when an array is allocated), the runtime decides if it should use the brk syscall for smaller chunks or mmap 为更大的块请求内存时。 sbrk
用于增加堆大小。分配的堆 space 可能会返回给 OS,但前提是堆顶部有足够的连续 space。这意味着恰好位于堆顶端的对象已经有几个字节可能会有效地阻止进程将任何堆内存返回给 OS。内存没有被浪费,因为 运行 时间将使用堆上其他已释放的 space 用于后续调用 malloc()
,但进程仍使用内存,因此从未报告为在进程终止之前可用。
通过 mmap()
分配内存页面效率较低,但它的好处是这些页面可以在不再需要时返回给 OS。性能下降是因为每当内存页面被映射或取消映射时,内核都会参与;特别是因为内核出于安全原因必须将映射页面清零。
mmap 阈值
malloc()
使用请求的内存量的阈值来决定是否应该使用堆或 mmap()
。这个阈值在最新版本的 glibc 中是动态的,但可以使用 mallopt() 函数更改:
M_MMAP_THRESHOLD
[...]
Note: Nowadays, glibc uses a dynamic mmap threshold by default. The initial value of the threshold is 128*1024, but when blocks larger than the current threshold and less than or equal to DEFAULT_MMAP_THRESHOLD_MAX are freed, the threshold is adjusted upwards to the size of the freed block. When dynamic mmap thresholding is in effect, the threshold for trimming the heap is also dynamically adjusted to be twice the dynamic mmap threshold. Dynamic adjustment of the mmap threshold is disabled if any of the M_TRIM_THRESHOLD, M_TOP_PAD, M_MMAP_THRESHOLD, or M_MMAP_MAX parameters is set.
至少可以通过两种方式调整阈值:
正在调用
mallopt()
。通过设置环境变量
MALLOC_MMAP_THRESHOLD_
(注意结尾的下划线)。
应用于示例代码
该示例以 2**24
字节或 16MiB 的块为单位分配(和释放)内存。根据该理论,固定的 MMAP_THRESHOLD
稍微低于此值应该确保所有大数组都使用 mmap()
分配,允许它们被取消映射并返回给 OS.
第一个运行没有修改:
$ ./test_mem.py
Iteration 0
Available memory: 21.45 GiB
Available memory: 2.17 GiB
Result: 1235
Available memory: 21.50 GiB
Iteration 0 ends
Iteration 1
Available memory: 21.50 GiB
Available memory: 2.13 GiB
Result: 1238
Available memory: 3.95 GiB
Iteration 1 ends
Iteration 2
Available memory: 4.02 GiB
Available memory: 4.02 GiB
Result: 232
Available memory: 4.02 GiB
Iteration 2 ends
Program done.
Available memory: 4.02 GiB
迭代 1 和 2 中未返回内存。
现在让我们设置 1MiB 的固定阈值:
$ MALLOC_MMAP_THRESHOLD_=1048576 ./test_mem.py
Iteration 0
Available memory: 21.55 GiB
Available memory: 2.13 GiB
Result: 1241
Available memory: 21.52 GiB
Iteration 0 ends
Iteration 1
Available memory: 21.52 GiB
Available memory: 2.11 GiB
Result: 1240
Available memory: 21.52 GiB
Iteration 1 ends
Iteration 2
Available memory: 21.51 GiB
Available memory: 2.12 GiB
Result: 1239
Available memory: 21.53 GiB
Iteration 2 ends
Program done.
Available memory: 21.53 GiB
可以看出,在所有三个迭代中,内存都成功返回给 OS。
作为替代方案,也可以通过使用 ctypes 模块调用 mallopt()
将设置集成到 Python 脚本中:
#!/usr/bin/env python3
import ctypes
import psutil
import time
import queue
import numpy as np
libc = ctypes.cdll.LoadLibrary("libc.so.6")
M_MMAP_THRESHOLD = -3
# Set malloc mmap threshold.
libc.mallopt(M_MMAP_THRESHOLD, 2**20)
# ...
免责声明:这些 solutions/workarounds 与平台无关,因为它们使用特定的 glibc 功能。
备注
以上文字主要回答了比较重要的第二个问题“如何强制解释器将保留内存还给OS,从而使报告的可用内存实际增加?”。至于第一个问题“我如何查询解释器的内存管理以找出引用对象使用了多少内存,以及仅保留供将来使用而不还给 OS 的内存量?”,我是无法找到满意的答案。 malloc_stats()
:
libc = ctypes.cdll.LoadLibrary("libc.so.6")
# ... script here ...
libc.malloc_stats()
给出了一些数字,但是这些结果:
Arena 0:
system bytes = 1632264192
in use bytes = 4629984
Total (incl. mmap):
system bytes = 1632858112
in use bytes = 5223904
max mmap regions = 1236
max mmap bytes = 20725514240
对于脚本 运行 不更改 mmap 阈值 我觉得有点困惑。 5MiB 可能是脚本结束时实际使用的内存,但是“系统字节”呢?该进程此时仍使用近 20GiB,因此指示的 1.6GiB 根本不适合图片。
另见
Reduce memory fragmentation with MALLOC_MMAP_THRESHOLD_ and MALLOC_MMAP_MAX_
Releasing memory in Python