使用malloc而非PyMem_Malloc有什么理由吗?

9 投票
3 回答
7409 浏览
提问于 2025-04-16 10:45

我正在阅读关于Python C扩展中的内存管理的文档,感觉用malloc似乎没有太多必要,反而用PyMem_Malloc更好。假设我想分配一个数组,这个数组不会被Python的源代码直接使用,并且会存储在一个会被垃圾回收的对象里。那还有必要使用malloc吗?

3 个回答

2

根据我写MATLAB .mex函数的经验,我觉得决定是否使用malloc的最大因素是可移植性。比如说,你有一个头文件,它只使用内部的C数据类型来执行很多有用的功能(不需要和Python对象互动,所以使用malloc没有问题),但你突然意识到你想把这个头文件移植到一个完全与Python无关的代码库中(可能是一个纯C写的项目),那么使用malloc显然会是一个更可移植的解决方案。

但是对于纯粹是Python扩展的代码,我最初的反应是期待原生的C函数会运行得更快。虽然我没有证据来支持这个观点 :)

7

扩展程序使用malloc或其他系统分配器来分配内存是完全可以的。这在很多模块中是正常且不可避免的,尤其是那些封装了其他库的模块,因为这些库本身并不知道Python的存在,所以在这些库中进行操作时会导致本地内存分配。(有些库允许你控制内存分配,以避免这种情况;但大多数库并不支持。)

使用PyMem_Malloc有一个严重的缺点:在使用它时,你需要保持GIL(全局解释器锁)。本地库在进行CPU密集型计算或进行可能会阻塞的操作(比如输入输出)时,通常希望释放GIL。在分配内存之前需要锁定GIL,这可能会让事情变得非常麻烦,甚至影响性能。

使用Python的内存分配包装函数可以让Python的内存调试代码发挥作用。不过,像Valgrind这样的工具在实际应用中对此的价值我并不太确定。

如果某个API要求使用这些函数,你就需要使用它们;例如,如果一个API接收一个指针,而这个指针必须用这些函数分配,这样才能用它们释放。除非有明确的理由需要使用这些函数,否则我还是更倾向于使用普通的内存分配方式。

8

编辑: 纠正了混用 PyMem_MallocPyObject_Malloc 的问题;它们是两个不同的调用。

在没有激活 PYMALLOC_DEBUG 宏的情况下,PyMem_Malloc 实际上是 libc 的 malloc() 的一个别名,有一个特殊的情况:调用 PyMem_Malloc 来分配零字节时,会返回一个非空指针,而调用 malloc(zero_bytes) 可能会返回空值或引发系统错误(源代码参考):

/* malloc。注意 nbytes==0 尝试返回一个非空指针,

  • 与所有其他当前存在的指针不同。这可能无法实现。

此外,在 pymem.h 头文件 中有一个建议说明:

切勿将 PyMem_ 的调用与平台的 malloc/realloc/calloc/free 混合使用。例如,在 Windows 上,不同的 DLL 可能会使用不同的堆,如果你使用 PyMem_Malloc,你将从 Python DLL 使用的堆中获取内存;如果你直接在自己的扩展中调用 free(),可能会造成灾难。使用 PyMem_Free 可以确保 Python 将内存返回到正确的堆中。另一个例子是,在 PYMALLOC_DEBUG 模式下,Python 会将所有对 PyMem_ 和 PyObject_ 内存函数的调用包装在特殊的调试包装器中,这些包装器会为动态内存块添加额外的调试信息。系统例程不知道该如何处理这些信息,而 Python 的包装器也不知道该如何处理直接由系统例程获得的原始内存块。

接下来,PyObject_Malloc 里面有一些 Python 特有的优化,这个函数不仅用于 C 扩展,还用于运行 Python 程序时的所有动态分配,比如 100*234str(100)10 + 4j

>>> id(10 + 4j)
139721697591440
>>> id(10 + 4j)
139721697591504
>>> id(10 + 4j)
139721697591440

之前的 complex() 实例是分配在专用池中的小对象。

使用 PyObject_Malloc 分配小对象(小于 256 字节)非常高效,因为它是从一个对齐到 8 字节的块池中分配的,每种块大小都有一个池。对于更大的分配,还有页面和区域块。

关于 源代码 的评论解释了 PyObject_Malloc 调用是如何优化的:

/*
 * The basic blocks are ordered by decreasing execution frequency,
 * which minimizes the number of jumps in the most common cases,
 * improves branching prediction and instruction scheduling (small
 * block allocations typically result in a couple of instructions).
 * Unless the optimizer reorders everything, being too smart...
 */

池、页面和区域是为了减少长时间运行的 Python 程序的 外部内存碎片 而进行的优化。

查看 源代码,获取关于 Python 内存内部结构的详细文档。

撰写回答