博主正在使用 Cython 开发一个 Python 扩展模块,该模块封装了一个用 C++ 编写的类。在使用该模块处理数据时,程序在终止前大约四分之一的概率会出现段错误 (Segmentation Fault),但所有数据都已正确处理。
博主已经尝试使用 GDB 调试,但发现段错误发生在 Python 代码之外。并且,无论在 ipython 中运行代码、更改解释器/ipython,还是将 Cython 从 1.9.1 降级到 1.6,问题依然存在。
解决方案
通过 Valgrind 分析,博主发现问题并非 C++ 代码中的内存泄露,而是 Cython 代码中的内存泄露。博主在 Cython 代码中发现了一段存在问题的代码:
def split_props(np.ndarray[fptype, ndim=1, mode="c"] x,
np.ndarray[fptype, ndim=1, mode="c"] y,
np.ndarray[fptype, ndim=1, mode="c"] ylines):
cdef np.ndarray[fptype, ndim = 1, mode = "c"] areas = np.zeros((ylines.shape[0] + 1), dtype=np.float64, order="c")
cdef np.ndarray[fptype, ndim = 1, mode = "c"] static_moments_x = np.zeros((ylines.shape[0] + 1), dtype=np.float64, order="c")
cdef np.ndarray[fptype, ndim = 1, mode = "c"] inertia_moments_xx = np.zeros((ylines.shape[0] + 1), dtype=np.float64, order="c")
cdef SplitPolygon * SPINSTANCE = new SplitPolygon(100, 100)
SPINSTANCE.split_props(& x[0], # = <fptype *> x.data
& y[0],
x.shape[0],
& ylines[0],
ylines.shape[0],
& areas[0],
& static_moments_x[0],
& inertia_moments_xx[0]
)
del SPINSTANCE
return areas, static_moments_x, inertia_moments_xx
在这段代码中,博主发现问题出在 SplitPolygon.split_props()
函数的调用上。该函数将 x
、y
和 ylines
数组作为参数,并在内部对这些数组进行操作。然而,博主在传递这些数组时并没有正确地遵守 SplitPolygon
类的规范,导致 SplitPolygon.split_props()
函数产生了未定义的行为。
在这种情况下,未定义的行为是 SplitPolygon.split_props()
函数在 x
、y
和 ylines
数组的边界之外写入数据。这些数组是由 Numpy 使用 zeros()
函数分配的,因此损坏的内存位于它们的附近。在不同的运行中,损坏的内存可能属于一个关键的 Python 对象,从而导致段错误,也可能不属于任何关键对象,从而导致程序正常运行。
代码例子
博主提供了两个 Cython 函数的代码示例,第一个函数 split_props()
在调用 C++ 代码时存在问题,第二个函数 SplitCirc()
则没有这个问题:
# 有问题的函数
def split_props(np.ndarray[fptype, ndim=1, mode="c"] x,
np.ndarray[fptype, ndim=1, mode="c"] y,
np.ndarray[fptype, ndim=1, mode="c"] ylines):
cdef np.ndarray[fptype, ndim = 1, mode = "c"] areas = np.zeros((ylines.shape[0] + 1), dtype=np.float64, order="c")
cdef np.ndarray[fptype, ndim = 1, mode = "c"] static_moments_x = np.zeros((ylines.shape[0] + 1), dtype=np.float64, order="c")
cdef np.ndarray[fptype, ndim = 1, mode = "c"] inertia_moments_xx = np.zeros((ylines.shape[0] + 1), dtype=np.float64, order="c")
cdef SplitPolygon * SPINSTANCE = new SplitPolygon(100, 100)
SPINSTANCE.split_props(& x[0], # = <fptype *> x.data
& y[0],
x.shape[0],
& ylines[0],
ylines.shape[0],
& areas[0],
& static_moments_x[0],
& inertia_moments_xx[0]
)
del SPINSTANCE
return areas, static_moments_x, inertia_moments_xx
# 没有问题的函数
def SplitCirc(np.ndarray[fptype, ndim=1, mode="c"] ycenters,
np.ndarray[fptype, ndim=1, mode="c"] radii,
np.ndarray[fptype, ndim=1, mode="c"] ylines):
cdef np.ndarray[fptype, ndim = 1, mode = "c"] areas = np.zeros((len(ylines) + 1), dtype=np.float64, order="c")
cdef np.ndarray[fptype, ndim = 1, mode = "c"] static_moments_x = np.zeros((len(ylines) + 1), dtype=np.float64, order="c")
cdef np.ndarray[fptype, ndim = 1, mode = "c"] inertia_moments_xx = np.zeros((len(ylines) + 1), dtype=np.float64, order="c")
split_circles(& ycenters[0], & radii[0], len(ycenters),
& ylines[0], len(ylines),
& areas[0], & static_moments_x[0], & inertia_moments_xx[0])
return areas, static_moments_x, inertia_moments_xx
博主通过修复 split_props()
函数中对 SplitPolygon.split_props()
函数的调用,解决了内存泄露问题。博主也提供了另一个 SplitCirc()
函数的代码示例,该函数没有内存泄露问题。