使用malloc而非PyMem_Malloc有什么理由吗?
我正在阅读关于Python C扩展中的内存管理的文档,感觉用malloc
似乎没有太多必要,反而用PyMem_Malloc
更好。假设我想分配一个数组,这个数组不会被Python的源代码直接使用,并且会存储在一个会被垃圾回收的对象里。那还有必要使用malloc
吗?
3 个回答
根据我写MATLAB .mex函数的经验,我觉得决定是否使用malloc的最大因素是可移植性。比如说,你有一个头文件,它只使用内部的C数据类型来执行很多有用的功能(不需要和Python对象互动,所以使用malloc没有问题),但你突然意识到你想把这个头文件移植到一个完全与Python无关的代码库中(可能是一个纯C写的项目),那么使用malloc显然会是一个更可移植的解决方案。
但是对于纯粹是Python扩展的代码,我最初的反应是期待原生的C函数会运行得更快。虽然我没有证据来支持这个观点 :)
扩展程序使用malloc或其他系统分配器来分配内存是完全可以的。这在很多模块中是正常且不可避免的,尤其是那些封装了其他库的模块,因为这些库本身并不知道Python的存在,所以在这些库中进行操作时会导致本地内存分配。(有些库允许你控制内存分配,以避免这种情况;但大多数库并不支持。)
使用PyMem_Malloc有一个严重的缺点:在使用它时,你需要保持GIL(全局解释器锁)。本地库在进行CPU密集型计算或进行可能会阻塞的操作(比如输入输出)时,通常希望释放GIL。在分配内存之前需要锁定GIL,这可能会让事情变得非常麻烦,甚至影响性能。
使用Python的内存分配包装函数可以让Python的内存调试代码发挥作用。不过,像Valgrind这样的工具在实际应用中对此的价值我并不太确定。
如果某个API要求使用这些函数,你就需要使用它们;例如,如果一个API接收一个指针,而这个指针必须用这些函数分配,这样才能用它们释放。除非有明确的理由需要使用这些函数,否则我还是更倾向于使用普通的内存分配方式。
编辑: 纠正了混用 PyMem_Malloc
和 PyObject_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*234
、str(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 内存内部结构的详细文档。