Python:ctypes中的垃圾收集器行为

16

假设我写了一些C/C++代码,它会分配一些内存,并返回一个指向它的指针。

#include <stdlib.h>

#ifdef __cplusplus
  extern "C" {
#endif

void Allocate(void **p) {
 int N=2048;
 *p=malloc(N);
}

#ifdef __cplusplus
 }
#endif

显然,我希望负责释放那块内存。现在假设我将其编译成共享库,并使用ctypes从Python中调用它,但没有显式释放该内存。

import ctypes
from ctypes import cdll, Structure, byref
external_lib = cdll.LoadLibrary('libtest.so.1.0')
ptr=ctypes.c_void_p(0)
external_lib.Allocate(ctypes.byref(ptr))

如果我使用valgrind运行此脚本,在不使用'-O3'标志编译test.cpp的情况下,我会得到2048字节的内存泄漏。但是如果我使用'-O3'标志进行编译,则不会出现内存泄漏。

这并不是真正的问题——我总是小心地明确释放我分配的任何内存。但我很好奇这种行为来自哪里。

我在Linux中使用以下脚本进行了测试。

g++ -Wall -c -fPIC -fno-common test.cpp -o libtest1.o
g++ -shared -Wl,-soname,libtest1.so.1 -o libtest1.so.1.0  libtest1.o

g++ -O3 -Wall -c -fPIC -fno-common test.cpp -o libtest2.o
g++ -shared -Wl,-soname,libtest2.so.1 -o libtest2.so.1.0  libtest2.o

valgrind python test1.py &> report1
valgrind python test2.py &> report2

以下是输出结果:

报告1:

==27875== LEAK SUMMARY:
==27875==    definitely lost: 2,048 bytes in 1 blocks
==27875==    indirectly lost: 0 bytes in 0 blocks
==27875==      possibly lost: 295,735 bytes in 1,194 blocks
==27875==    still reachable: 744,633 bytes in 5,025 blocks
==27875==         suppressed: 0 bytes in 0 blocks

报告2:

==27878== LEAK SUMMARY:
==27878==    definitely lost: 0 bytes in 0 blocks
==27878==    indirectly lost: 0 bytes in 0 blocks
==27878==      possibly lost: 295,735 bytes in 1,194 blocks
==27878==    still reachable: 746,681 bytes in 5,026 blocks
==27878==         suppressed: 0 bytes in 0 blocks

1
我按照你的步骤进行了操作,结果很有趣。在 Python 3.3.2 中,这两个报告都显示泄漏了2048字节,但是在 Python 2.7.5 中,这两个报告都没有泄漏。在 Linux 3.11.4 x86_64 上测试,使用的编译器是 gcc 4.8.1 20130725 - starrify
2个回答

4
不同的用户在其平台上似乎会得到不同的结果。我尝试在Debian Wheezy系统上使用Python 2.5.5、Python 2.6.8和Python 3.2.3以及g++ 4.7.2进行复制,但未能成功。
根据您的代码,您知道它是有泄漏的,只是valgrind报告内存使用情况不同。在报告1中,绝对没有提到2048块的参考。在报告2中,它在“仍可达”部分中列出。 Valgrind泄漏检测文档描述了如何检测内存泄漏。有趣的是,它会查找每个线程中内存和通用寄存器集中的引用。当泄漏检测器在程序退出时运行时,仍然可能存在CPU寄存器中含有已分配内存的引用,虽然这种情况可能不太可能发生。对于未优化版本,Allocate函数中可能存在附加指令,可以破坏任何可能包含泄漏引用的寄存器信息。在优化版本中,Allocate函数可能会保留寄存器中的引用,并将结果存储在*p中。
当然,如果无法重现此问题,那么一切只是猜测。您可以请求valgrind输出有关其发现的引用的更多信息,这可能会提供有关分配块的更多见解。
例如:这将显示可达和不可达块。
valgrind --show-reachable=yes --leak-check=full python2.5 test1.py &> report1-2.5

如果我将您的代码修改为以下内容,则我的系统上所有测试都表明2048块肯定已丢失(即使已分配4096字节)。这也让我相信它可能是某种被valgrind泄漏检测器捕获的缓存寄存器值。
import ctypes
from ctypes import cdll, Structure, byref
external_lib = cdll.LoadLibrary('libtest.so.1.0')
ptr=ctypes.c_void_p(0)
external_lib.Allocate(ctypes.byref(ptr))
external_lib.Allocate(ctypes.byref(ptr))  # <-- Allocate a second block, the first becomes lost.

这是从valgrind得出的代码片段,显示了可访问和不可访问的块:
==28844== 2,048 bytes in 1 blocks are still reachable in loss record 305 of 366
==28844==    at 0x4C28BED: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==28844==    by 0x6CD870F: Allocate (in /projects/stack-overflow/18929183-python-garbage-collector-behavior-with-ctypes/libtest1.so.1.0)
==28844==    by 0x6ACEDEF: ffi_call_unix64 (in /usr/lib/python2.6/lib-dynload/_ctypes.so)
==28844==    by 0x6ACE86A: ffi_call (in /usr/lib/python2.6/lib-dynload/_ctypes.so)
==28844==    by 0x6AC9A66: _CallProc (callproc.c:816)
==28844==    by 0x6AC136C: CFuncPtr_call (_ctypes.c:3860)
==28844==    by 0x424989: PyObject_Call (abstract.c:2492)
==28844==    by 0x4A17B8: PyEval_EvalFrameEx (ceval.c:3968)
==28844==    by 0x49F0D1: PyEval_EvalCodeEx (ceval.c:3000)
==28844==    by 0x49F211: PyEval_EvalCode (ceval.c:541)
==28844==    by 0x4C66FE: PyRun_FileExFlags (pythonrun.c:1358)
==28844==    by 0x4C7A36: PyRun_SimpleFileExFlags (pythonrun.c:948)
==28844==
==28844== 2,048 bytes in 1 blocks are definitely lost in loss record 306 of 366
==28844==    at 0x4C28BED: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==28844==    by 0x6CD870F: Allocate (in /projects/stack-overflow/18929183-python-garbage-collector-behavior-with-ctypes/libtest1.so.1.0)
==28844==    by 0x6ACEDEF: ffi_call_unix64 (in /usr/lib/python2.6/lib-dynload/_ctypes.so)
==28844==    by 0x6ACE86A: ffi_call (in /usr/lib/python2.6/lib-dynload/_ctypes.so)
==28844==    by 0x6AC9A66: _CallProc (callproc.c:816)
==28844==    by 0x6AC136C: CFuncPtr_call (_ctypes.c:3860)
==28844==    by 0x424989: PyObject_Call (abstract.c:2492)
==28844==    by 0x4A17B8: PyEval_EvalFrameEx (ceval.c:3968)
==28844==    by 0x49F0D1: PyEval_EvalCodeEx (ceval.c:3000)
==28844==    by 0x49F211: PyEval_EvalCode (ceval.c:541)
==28844==    by 0x4C66FE: PyRun_FileExFlags (pythonrun.c:1358)
==28844==    by 0x4C7A36: PyRun_SimpleFileExFlags (pythonrun.c:948)

-1

这种行为来自于gcc -O3优化。gcc看到分配的内存未被使用,因此省略了这段代码块。

您可以参考以下问题:malloc and gcc optimization 2


我不认为这里是这种情况。在你提到的问题中,分配的地址从未离开循环并且从未被使用。而在这里,该地址通过参数指针写入某个位置,因此不能被标记为“未使用”。 - viraptor
请至少尝试反汇编二进制文件或查看gcc -S的结果,以验证包含malloc的代码是否会被消除。 - starrify

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接