PEP 445 – 添加新的 API 以自定义 Python 内存分配器
- 作者:
- Victor Stinner <vstinner at python.org>
- BDFL 委托:
- Antoine Pitrou <solipsis at pitrou.net>
- 状态:
- 最终版
- 类型:
- 标准跟踪
- 创建日期:
- 2013年6月15日
- Python 版本:
- 3.4
- 决议:
- Python-Dev 消息
摘要
本 PEP 提出了新的应用程序编程接口(API)以自定义 Python 内存分配器。唯一需要符合本 PEP 的实现是 CPython,但其他实现可以选择兼容,或重用类似的方案。
基本原理
用例
- 嵌入 Python 的应用程序希望将 Python 内存与应用程序内存隔离,或者希望使用针对其 Python 用途优化的不同内存分配器
- Python 运行在内存不足和 CPU 缓慢的嵌入式设备上。可以使用自定义内存分配器来提高效率和/或访问设备的所有内存。
- 内存分配器的调试工具
- 跟踪内存使用情况(查找内存泄漏)
- 获取内存分配的位置:Python 文件名和行号,以及内存块的大小
- 检测缓冲区下溢、缓冲区溢出和滥用 Python 分配器 API(参见将内存块分配器上的调试检查重新设计为钩子)
- 强制内存分配失败以测试
MemoryError异常的处理
提案
新函数和结构
- 添加新的 GIL-free(无需持有 GIL)内存分配器
void* PyMem_RawMalloc(size_t size)void* PyMem_RawRealloc(void *ptr, size_t new_size)void PyMem_RawFree(void *ptr)- 新分配的内存将不会以任何方式初始化。
- 如果可能,请求零字节会返回一个不同的非 NULL 指针,就像调用了
PyMem_Malloc(1)一样。
- 添加新的
PyMemAllocator结构typedef struct { /* user context passed as the first argument to the 3 functions */ void *ctx; /* allocate a memory block */ void* (*malloc) (void *ctx, size_t size); /* allocate or resize a memory block */ void* (*realloc) (void *ctx, void *ptr, size_t new_size); /* release a memory block */ void (*free) (void *ctx, void *ptr); } PyMemAllocator;
- 添加新的
PyMemAllocatorDomain枚举以选择 Python 分配器域。域PYMEM_DOMAIN_RAW:PyMem_RawMalloc()、PyMem_RawRealloc()和PyMem_RawFree()PYMEM_DOMAIN_MEM:PyMem_Malloc()、PyMem_Realloc()和PyMem_Free()PYMEM_DOMAIN_OBJ:PyObject_Malloc()、PyObject_Realloc()和PyObject_Free()
- 添加新的函数以获取和设置内存块分配器
void PyMem_GetAllocator(PyMemAllocatorDomain domain, PyMemAllocator *allocator)void PyMem_SetAllocator(PyMemAllocatorDomain domain, PyMemAllocator *allocator)- 新分配器在请求零字节时必须返回一个不同的非 NULL 指针
- 对于
PYMEM_DOMAIN_RAW域,分配器必须是线程安全的:在调用分配器时不会持有 GIL。
- 添加新的
PyObjectArenaAllocator结构typedef struct { /* user context passed as the first argument to the 2 functions */ void *ctx; /* allocate an arena */ void* (*alloc) (void *ctx, size_t size); /* release an arena */ void (*free) (void *ctx, void *ptr, size_t size); } PyObjectArenaAllocator;
- 添加新的函数以获取和设置 pymalloc 使用的竞技场分配器
void PyObject_GetArenaAllocator(PyObjectArenaAllocator *allocator)void PyObject_SetArenaAllocator(PyObjectArenaAllocator *allocator)
- 添加一个新函数,用于在内存分配器被
PyMem_SetAllocator()替换时重新安装内存分配器上的调试检查void PyMem_SetupDebugHooks(void)- 在所有内存块分配器上安装调试钩子。该函数可以多次调用,钩子只安装一次。
- 如果 Python 未以调试模式编译,该函数不执行任何操作。
- 如果 size 大于
PY_SSIZE_T_MAX,内存块分配器总是返回 NULL。在调用内部函数之前进行检查。
注意
pymalloc 分配器针对小于 512 字节且生命周期短的对象进行了优化。它使用固定大小为 256 KB 的内存映射,称为“竞技场”。
下面是默认情况下分配器的设置方式
PYMEM_DOMAIN_RAW,PYMEM_DOMAIN_MEM:malloc(),realloc()和free(); 请求零字节时调用malloc(1)PYMEM_DOMAIN_OBJ: pymalloc 分配器,对于大于 512 字节的分配,回退到PyMem_Malloc()- pymalloc 竞技场分配器:在 Windows 上是
VirtualAlloc()和VirtualFree(),可用时是mmap()和munmap(),否则是malloc()和free()
将内存块分配器上的调试检查重新设计为钩子
自 Python 2.3 以来,Python 在调试模式下对内存分配器实施了不同的检查
- 新分配的内存用字节
0xCB填充,释放的内存用字节0xDB填充。 - 检测 API 违规,例如:在
PyMem_Malloc()分配的内存块上调用PyObject_Free() - 检测缓冲区开始前写入(缓冲区下溢)
- 检测缓冲区结束后写入(缓冲区溢出)
在 Python 3.3 中,通过使用宏替换 PyMem_Malloc()、PyMem_Realloc()、PyMem_Free()、PyObject_Malloc()、PyObject_Realloc() 和 PyObject_Free() 来安装这些检查。新的分配器分配一个更大的缓冲区,并写入一个模式以检测缓冲区下溢、缓冲区溢出和释放后使用(通过用字节 0xDB 填充缓冲区)。它使用原始的 PyObject_Malloc() 函数来分配内存。因此,PyMem_Malloc() 和 PyMem_Realloc() 间接调用 PyObject_Malloc() 和 PyObject_Realloc()。
本 PEP 将调试检查重新设计为调试模式下现有分配器上的钩子。没有钩子时的调用跟踪示例
PyMem_RawMalloc()=>_PyMem_RawMalloc()=>malloc()PyMem_Realloc()=>_PyMem_RawRealloc()=>realloc()PyObject_Free()=>_PyObject_Free()
安装钩子后的调用跟踪(调试模式)
PyMem_RawMalloc()=>_PyMem_DebugMalloc()=>_PyMem_RawMalloc()=>malloc()PyMem_Realloc()=>_PyMem_DebugRealloc()=>_PyMem_RawRealloc()=>realloc()PyObject_Free()=>_PyMem_DebugFree()=>_PyObject_Free()
结果是,PyMem_Malloc() 和 PyMem_Realloc() 现在在发布模式和调试模式下都调用 malloc() 和 realloc(),而不是在调试模式下调用 PyObject_Malloc() 和 PyObject_Realloc()。
当至少一个内存分配器被 PyMem_SetAllocator() 替换时,必须调用 PyMem_SetupDebugHooks() 函数,以便在新分配器之上重新安装调试钩子。
不再直接调用 malloc()
PyObject_Malloc() 回退到 PyMem_Malloc() 而不是 malloc(),如果大小大于或等于 512 字节,并且 PyObject_Realloc() 回退到 PyMem_Realloc() 而不是 realloc()
直接调用 malloc() 被替换为 PyMem_Malloc(),如果 GIL 没有被持有,则替换为 PyMem_RawMalloc()。
可以配置诸如 zlib 或 OpenSSL 之类的外部库使用 PyMem_Malloc() 或 PyMem_RawMalloc() 分配内存。如果库的分配器只能全局替换(而不是逐对象替换),则在 Python 嵌入到应用程序中时,不应替换它。
对于“跟踪内存使用情况”用例,跟踪外部库中分配的内存以获得准确的报告非常重要,因为这些分配可能很大(例如,它们可能引发 MemoryError 异常),否则将在内存使用报告中遗漏。
示例
用例 1:替换内存分配器,保留 pymalloc
每次内存块浪费 2 字节,每个 pymalloc 竞技场浪费 10 字节的虚拟示例
#include <stdlib.h>
size_t alloc_padding = 2;
size_t arena_padding = 10;
void* my_malloc(void *ctx, size_t size)
{
int padding = *(int *)ctx;
return malloc(size + padding);
}
void* my_realloc(void *ctx, void *ptr, size_t new_size)
{
int padding = *(int *)ctx;
return realloc(ptr, new_size + padding);
}
void my_free(void *ctx, void *ptr)
{
free(ptr);
}
void* my_alloc_arena(void *ctx, size_t size)
{
int padding = *(int *)ctx;
return malloc(size + padding);
}
void my_free_arena(void *ctx, void *ptr, size_t size)
{
free(ptr);
}
void setup_custom_allocator(void)
{
PyMemAllocator alloc;
PyObjectArenaAllocator arena;
alloc.ctx = &alloc_padding;
alloc.malloc = my_malloc;
alloc.realloc = my_realloc;
alloc.free = my_free;
PyMem_SetAllocator(PYMEM_DOMAIN_RAW, &alloc);
PyMem_SetAllocator(PYMEM_DOMAIN_MEM, &alloc);
/* leave PYMEM_DOMAIN_OBJ unchanged, use pymalloc */
arena.ctx = &arena_padding;
arena.alloc = my_alloc_arena;
arena.free = my_free_arena;
PyObject_SetArenaAllocator(&arena);
PyMem_SetupDebugHooks();
}
用例 2:替换内存分配器,覆盖 pymalloc
如果您有一个专用的分配器,针对小于 512 字节且生命周期短的对象分配进行了优化,那么可以覆盖 pymalloc(替换 PyObject_Malloc())。
每次内存块浪费 2 字节的虚拟示例
#include <stdlib.h>
size_t padding = 2;
void* my_malloc(void *ctx, size_t size)
{
int padding = *(int *)ctx;
return malloc(size + padding);
}
void* my_realloc(void *ctx, void *ptr, size_t new_size)
{
int padding = *(int *)ctx;
return realloc(ptr, new_size + padding);
}
void my_free(void *ctx, void *ptr)
{
free(ptr);
}
void setup_custom_allocator(void)
{
PyMemAllocator alloc;
alloc.ctx = &padding;
alloc.malloc = my_malloc;
alloc.realloc = my_realloc;
alloc.free = my_free;
PyMem_SetAllocator(PYMEM_DOMAIN_RAW, &alloc);
PyMem_SetAllocator(PYMEM_DOMAIN_MEM, &alloc);
PyMem_SetAllocator(PYMEM_DOMAIN_OBJ, &alloc);
PyMem_SetupDebugHooks();
}
不需要替换 pymalloc 竞技场,因为它不再被新分配器使用。
用例 3:在内存块分配器上设置钩子
在所有内存块分配器上设置钩子的示例
struct {
PyMemAllocator raw;
PyMemAllocator mem;
PyMemAllocator obj;
/* ... */
} hook;
static void* hook_malloc(void *ctx, size_t size)
{
PyMemAllocator *alloc = (PyMemAllocator *)ctx;
void *ptr;
/* ... */
ptr = alloc->malloc(alloc->ctx, size);
/* ... */
return ptr;
}
static void* hook_realloc(void *ctx, void *ptr, size_t new_size)
{
PyMemAllocator *alloc = (PyMemAllocator *)ctx;
void *ptr2;
/* ... */
ptr2 = alloc->realloc(alloc->ctx, ptr, new_size);
/* ... */
return ptr2;
}
static void hook_free(void *ctx, void *ptr)
{
PyMemAllocator *alloc = (PyMemAllocator *)ctx;
/* ... */
alloc->free(alloc->ctx, ptr);
/* ... */
}
void setup_hooks(void)
{
PyMemAllocator alloc;
static int installed = 0;
if (installed)
return;
installed = 1;
alloc.malloc = hook_malloc;
alloc.realloc = hook_realloc;
alloc.free = hook_free;
PyMem_GetAllocator(PYMEM_DOMAIN_RAW, &hook.raw);
PyMem_GetAllocator(PYMEM_DOMAIN_MEM, &hook.mem);
PyMem_GetAllocator(PYMEM_DOMAIN_OBJ, &hook.obj);
alloc.ctx = &hook.raw;
PyMem_SetAllocator(PYMEM_DOMAIN_RAW, &alloc);
alloc.ctx = &hook.mem;
PyMem_SetAllocator(PYMEM_DOMAIN_MEM, &alloc);
alloc.ctx = &hook.obj;
PyMem_SetAllocator(PYMEM_DOMAIN_OBJ, &alloc);
}
注意
不需要调用 PyMem_SetupDebugHooks(),因为内存分配器没有被替换:内存块分配器上的调试检查在启动时自动安装。
性能
此 PEP 的实现(问题 #3329)对 Python 基准测试套件没有明显开销。
Python 基准测试套件 (http://hg.python.org/benchmarks) (-b 2n3) 的结果:一些测试快 1.04 倍,一些测试慢 1.04 倍。pybench 微基准测试的结果:“+0.1%”全局变慢(介于 -4.9% 和 +5.6% 之间)。
基准测试的完整输出已附加到问题 #3329。
被拒绝的替代方案
更具体的获取/设置内存分配器的函数
最初提议了一组更大的 C API 函数,每个分配器域都有一对函数
void PyMem_GetRawAllocator(PyMemAllocator *allocator)void PyMem_GetAllocator(PyMemAllocator *allocator)void PyObject_GetAllocator(PyMemAllocator *allocator)void PyMem_SetRawAllocator(PyMemAllocator *allocator)void PyMem_SetAllocator(PyMemAllocator *allocator)void PyObject_SetAllocator(PyMemAllocator *allocator)
此替代方案被拒绝,因为使用更具体的函数无法编写通用代码:每个内存分配器域都必须复制代码。
默认情况下使 PyMem_Malloc() 重用 PyMem_RawMalloc()
如果 PyMem_Malloc() 默认调用 PyMem_RawMalloc(),那么调用 PyMem_SetAllocator(PYMEM_DOMAIN_RAW, alloc) 也将间接修补 PyMem_Malloc()。
此替代方案被拒绝,因为 PyMem_SetAllocator() 将根据域具有不同的行为。始终保持相同的行为更不容易出错。
添加新的 PYDEBUGMALLOC 环境变量
有人提议添加一个新的 PYDEBUGMALLOC 环境变量,以启用内存块分配器上的调试检查。它将与调用 PyMem_SetupDebugHooks() 具有相同的效果,而无需编写任何 C 代码。另一个优点是即使在发布模式下也可以启用调试检查:调试检查将始终被编译进去,但只有当环境变量存在且非空时才启用。
这个替代方案被拒绝了,因为新的环境变量会使 Python 初始化更加复杂。PEP 432 试图简化 CPython 的启动序列。
使用宏获取可自定义的分配器
为了在默认配置下没有开销,可定制的分配器将是一个可选功能,通过配置选项或宏启用。
此替代方案被拒绝,因为使用宏意味着必须重新编译扩展模块才能使用新的分配器和分配器钩子。无需重新编译 Python 和扩展模块使得调试钩子在实践中更易于使用。
传递 C 文件名和行号
使用 __FILE__ 和 __LINE__ 将分配器函数定义为宏,以获取内存分配的 C 文件名和行号。
修改后的 PyMemAllocator 结构的 PyMem_Malloc 宏示例
typedef struct {
/* user context passed as the first argument
to the 3 functions */
void *ctx;
/* allocate a memory block */
void* (*malloc) (void *ctx, const char *filename, int lineno,
size_t size);
/* allocate or resize a memory block */
void* (*realloc) (void *ctx, const char *filename, int lineno,
void *ptr, size_t new_size);
/* release a memory block */
void (*free) (void *ctx, const char *filename, int lineno,
void *ptr);
} PyMemAllocator;
void* _PyMem_MallocTrace(const char *filename, int lineno,
size_t size);
/* the function is still needed for the Python stable ABI */
void* PyMem_Malloc(size_t size);
#define PyMem_Malloc(size) \
_PyMem_MallocTrace(__FILE__, __LINE__, size)
GC 分配器函数也必须打补丁。例如,_PyObject_GC_Malloc() 在许多 C 函数中使用,因此不同类型的对象将具有相同的分配位置。
此替代方案被拒绝,因为向每个分配器传递文件名和行号会使 API 更加复杂:向每个分配器函数传递 3 个新参数(ctx、filename、lineno),而不是仅仅一个上下文参数(ctx)。还需要修改 GC 分配器函数,这会增加太多复杂性,而收益却很小。
GIL-free PyMem_Malloc()
在 Python 3.3 中,当 Python 以调试模式编译时,PyMem_Malloc() 间接调用 PyObject_Malloc(),这需要持有 GIL(它不是线程安全的)。这就是为什么 PyMem_Malloc() 必须在持有 GIL 的情况下调用。
本 PEP 更改了 PyMem_Malloc():它现在总是调用 malloc() 而不是 PyObject_Malloc()。因此,可以从 PyMem_Malloc() 中移除“必须持有 GIL”的限制。
此替代方案被拒绝,因为允许在不持有 GIL 的情况下调用 PyMem_Malloc() 可能会破坏设置其自己的分配器或分配器钩子的应用程序。持有 GIL 对于开发自定义分配器很方便:无需关心其他线程。对于调试分配器钩子也很方便:Python 对象可以安全地检查,并且 C API 可以用于报告。
此外,在内存分配器中调用 PyGILState_Ensure() 会产生意外行为,尤其是在 Python 启动和创建新的 Python 线程状态时。最好将获取 GIL 的责任从自定义分配器中解放出来。
不添加 PyMem_RawMalloc()
将 malloc() 替换为 PyMem_Malloc(),但仅在持有 GIL 的情况下。否则,保持 malloc() 不变。
在某些 Python 函数中,PyMem_Malloc() 在没有持有 GIL 的情况下使用。例如,Python 的 main() 和 Py_Main() 函数调用 PyMem_Malloc(),而 GIL 尚不存在。在这种情况下,PyMem_Malloc() 将被替换为 malloc()(或 PyMem_RawMalloc())。
此替代方案被拒绝,因为 PyMem_RawMalloc() 对于准确的内存使用报告是必需的。当使用调试钩子跟踪内存使用情况时,无法跟踪直接调用 malloc() 分配的内存。PyMem_RawMalloc() 可以被钩住,因此可以跟踪 Python 分配的所有内存,包括在不持有 GIL 的情况下分配的内存。
使用现有调试工具分析内存使用情况
有许多现有的调试工具可用于分析内存使用情况。一些例子:Valgrind、Purify、Clang AddressSanitizer、failmalloc 等。
问题是如何检索与内存指针相关的 Python 对象以读取其类型和/或内容。另一个问题是检索内存分配的来源:C 回溯通常无用(与使用 __FILE__ 和 __LINE__ 的宏的理由相同,参见传递 C 文件名和行号),Python 文件名和行号(甚至 Python 回溯)更有用。
此替代方案被拒绝,因为经典工具无法内省 Python 内部以收集此类信息。能够在持有 GIL 的情况下设置分配器钩子可以收集大量来自 Python 内部的有用数据。
添加一个 msize() 函数
向 PyMemAllocator 和 PyObjectArenaAllocator 结构添加另一个函数
size_t msize(void *ptr);
此函数返回内存块或内存映射的大小。如果函数未实现或指针未知(例如:NULL 指针),则返回 (size_t)-1。
在 Windows 上,此函数可以使用 _msize() 和 VirtualQuery() 实现。
该函数可用于实现跟踪内存使用情况的钩子。分配器的 free() 方法只获取内存块的地址,而更新内存使用情况需要内存块的大小。
额外的 msize() 函数被拒绝,因为只有少数平台实现了它。例如,带 GNU libc 的 Linux 不提供获取内存块大小的函数。msize() 目前未在 Python 源代码中使用。该函数只用于跟踪内存使用,并使 API 更加复杂。调试钩子可以在内部实现该函数,无需将其添加到 PyMemAllocator 和 PyObjectArenaAllocator 结构中。
没有上下文参数
简化分配器函数的签名,移除上下文参数
void* malloc(size_t size)void* realloc(void *ptr, size_t new_size)void free(void *ptr)
分配器钩子很可能被 PyMem_SetAllocator() 和 PyObject_SetAllocator(),甚至 PyMem_SetRawAllocator() 重用,但钩子必须根据分配器调用不同的函数。上下文是一种方便的方法,可以为不同的 Python 分配器重用相同的自定义分配器或钩子。
在 C++ 中,上下文可以用来传递 this。
外部库
用于自定义内存分配器的 API 示例。
Python 使用的库
- OpenSSL: CRYPTO_set_mem_functions() 用于全局设置内存管理函数
- expat: parserCreate() 具有每实例内存处理器
- zlib: zlib 1.2.8 手册,传递一个不透明指针
- bz2: bzip2 和 libbzip2,版本 1.0.5,传递一个不透明指针
- lzma: LZMA SDK - 如何使用,传递一个不透明指针
- lipmpdec: 没有不透明指针(经典 malloc API)
其他库
- glib: g_mem_set_vtable()
- libxml2: xmlGcMemSetup(),全局
- Oracle 的 OCI: Oracle Call Interface 程序员指南,版本 2 (9.2),传递一个不透明指针
本 PEP 中新的 ctx 参数受到 zlib 和 Oracle OCI 库 API 的启发。
另请参阅 GNU libc: 内存分配钩子,它使用不同的方法来钩住内存分配器。
内存分配器
C 标准库提供了众所周知的 malloc() 函数。它的实现取决于平台和 C 库。GNU C 库使用修改后的 ptmalloc2,基于“Doug Lea 的 Malloc”(dlmalloc)。FreeBSD 使用 jemalloc。Google 提供了 tcmalloc,它是 gperftools 的一部分。
malloc() 使用两种内存:堆和内存映射。内存映射通常用于大型分配(例如:大于 256 KB),而堆用于小型分配。
在 UNIX 上,堆由 brk() 和 sbrk() 系统调用处理,并且是连续的。在 Windows 上,堆由 HeapAlloc() 处理,并且可以是不连续的。内存映射在 UNIX 上由 mmap() 处理,在 Windows 上由 VirtualAlloc() 处理,它们可以是不连续的。
释放内存映射会立即将内存返还给系统。在 UNIX 上,只有当释放的块位于堆的末尾时,堆内存才会返还给系统。否则,只有当释放的内存之后的所有内存也被释放时,内存才会返还给系统。
为了在堆上分配内存,分配器会尝试重用空闲空间。如果没有足够大的连续空间,即使有比所需大小更多的空闲空间,堆也必须扩大。这个问题被称为“内存碎片化”:系统看到的内存使用量高于实际使用量。在 Windows 上,如果连续空闲内存不足,HeapAlloc() 会使用 VirtualAlloc() 创建新的内存映射。
CPython 有一个 pymalloc 分配器,用于小于 512 字节的分配。此分配器针对生命周期短的小对象进行了优化。它使用固定大小为 256 KB 的内存映射,称为“竞技场”。
其他分配器
本 PEP 允许根据应用程序对内存的使用情况(分配次数、分配大小、对象生命周期等)精确选择使用的内存分配器。
链接
CPython 内存分配相关问题
- 问题 #3329: 添加新的 API 以自定义内存分配器
- 问题 #13483: 使用 VirtualAlloc 分配内存竞技场
- 问题 #16742: PyOS_Readline 释放 GIL 并调用 PyOS_StdioReadline,它不是线程安全的
- 问题 #18203: 将 malloc() 调用替换为 PyMem_Malloc() 或 PyMem_RawMalloc()
- 问题 #18227: 在 zlib 或 OpenSSL 等外部库中使用 Python 内存分配器
分析 Python 应用程序内存使用情况的项目
版权
本文档已进入公共领域。
来源: https://github.com/python/peps/blob/main/peps/pep-0445.rst
最后修改时间: 2025-02-01 08:59:27 GMT