Python ctypes的'c_char_p'内存泄漏问题

3

我正在开发一款用于加密的Python库。为了提高我的库的性能,我用GMP在C++中编写了主要类。我编写了C++类并编写了extern方法来使用主要算术操作:加法、减法等等...这些方法将结果作为char*返回,以避免类型转换问题。我构建了我的库的DLL,并在Python包装器中使用ctypes声明了这些方法。我注意到,在使用大量数字进行每个算术操作后,内存呈指数级增长。我一直在寻找C++实现中的问题,但由于C++垃圾回收器的原因,没有问题。最后,我找到了一个可能的解决方案,即我需要实现一个C++方法来释放DLL创建的字符串的内存。所以我写了这个简单的方法:

extern "C" {

    __declspec(dllexport) void free_memory(char * n)
    {
        free(n);
    }
    ...
}

我在Python包装器中实现了这段代码,以释放DLL分配的内存:

import os
import ctypes

DIR_PATH = os.path.dirname(os.path.realpath(__file__))
NUMERIC = ctypes.CDLL(DIR_PATH + "/numeric.dll")
...
NUMERIC.free_memory.argtypes = [ctypes.c_void_p]
NUMERIC.free_memory.restype = None

def void_cast(n):
    a = ctypes.cast(n, ctypes.c_char_p)
    res = ctypes.c_char_p(a.value)
    NUMERIC.free_memory(a)
    return res

因此,使用res = ctypes.c_char_p(a.value),我创建了一个不再指向a的新变量。这样,我可以正确地使用DLL方法删除a,但我仍然存在内存泄漏问题。似乎Python垃圾回收器不能正确释放c_char_p类型的字符串的内存。在以前的实现中,我只使用Python和gmpy2库,因此所有数字都被转换为mpzmpq。我使用memory_profiler包测试了内存消耗情况。我创建了40个椭圆曲线上定义的射影点对象,并计算了i*P的乘积,其中i从1到40。使用gmpy2总共使用了约70MB。而使用C++中的类与ctypes一起使用时,内存消耗升至1.5GB。很明显出现了问题,特别是当只有处理算术运算的基类发生变化时。如何正确释放内存,避免出现内存泄漏问题?我提供了一个用于计算算术运算的extern方法示例,但我已经确认问题仅在通过free_memory函数正确释放内存并重新分配字符串时,Python的垃圾回收器才会在需要时释放字符串。
extern "C" {
    __declspec(dllexport) const char* rat_add(const char * n, const char * m)
    {
        return (RationalNum(n) + RationalNum(m)).getValue();
    }
}

提前感谢,祝您拥有愉快的一天。

PS:在C++中,我正确实现了析构函数方法,以释放创建的mpz_tmpq_t对象所占用的空间。


“C++垃圾回收器”是什么意思?C++没有垃圾回收器。 - pschill
你如何在Python中使用rat_add?它可能会为getValue()返回的字符串分配内存,而这些内存可能无法正确释放。 - pschill
你能否创建一个最小可复现示例 - pschill
@pschill,“RationalNum”类没有释放其字符串。特别是,“RationalNum”只有一个类型为“mpq_t”的属性。基本上,它是一个包装器,用于使用“mpq_t”并在不调用“mpq_init”,“mpq_set”等的情况下执行所有“mpq_t”数字之间的操作...在析构函数中,我调用了“mpq_clear”。由于我没有使用“new”创建新对象,因此在关闭“rat_add”函数时,应该销毁这两个创建的对象,当我在C++上执行代码时,它似乎正在进行。 - G.F
@pschill,你想展示近1200行代码吗?无论如何,我已经尝试通过使用new显式调用RationalNum析构函数来创建操作数。对Python的影响是相同的,两者都会导致内存泄漏。唯一会创建内存的是作为字符串的操作结果。 - G.F
显示剩余6条评论
1个回答

1
问题出在这一行:
res = ctypes.c_char_p(a.value)

这将创建 a.value 的一个副本,并将 res 设置为指向该副本的 c_char_p。然而,Python 不会为 ctypes 指针进行内存管理,因此该副本将被泄漏!
如果您将上述行替换为以下内容,则可以解决泄漏问题:
res = bytes(memoryview(a.value))

这也会创建一个副本,但是res将成为一个真正的Python对象。


正如我所预料的那样...就像我之前写的那样,我不得不编写那行代码,因为free_memory也删除了res中的引用。我会修复它的!感谢您的回复。 - G.F
好的,我无法证明这个指令解决了问题,因为我意识到问题在于pow操作。当我计算pow(47, 300 * i)时,随着i的增长,C++上浪费了很多空间,而这些空间没有被释放。如果我在Python上这样做:pow(mpz (47), mpz(300) * i),占用的空间实际上是恒定的,不到100MB。我将不得不看看是否有可能在C++上修复这个问题。无论如何,感谢您的帮助。 - G.F

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