使用PyArray_SimpleNewFromData()创建数组并返回时,Python扩展程序中存在内存泄漏问题。

8

我编写了一个简单的Python扩展模块,用于模拟3位模拟数字转换器。它应该接受浮点数数组作为输入,并返回相同大小的输出数组。实际输出由量化的输入数字组成。这是我的(简化版)模块:

static PyObject *adc3(PyObject *self, PyObject *args) {
  PyArrayObject *inArray = NULL, *outArray = NULL;
  double *pinp = NULL, *pout = NULL;
  npy_intp nelem;
  int dims[1], i, j;

  /* Get arguments:  */
  if (!PyArg_ParseTuple(args, "O:adc3", &inArray))
    return NULL;

  nelem = PyArray_DIM(inArray,0); /* size of the input array */
  pout = (double *) malloc(nelem*sizeof(double));
  pinp = (double *) PyArray_DATA(inArray);

  /*   ADC action   */
  for (i = 0; i < nelem; i++) {
    if (pinp[i] >= -0.5) {
    if      (pinp[i] < 0.5)   pout[i] = 0;
    else if (pinp[i] < 1.5)   pout[i] = 1;
    else if (pinp[i] < 2.5)   pout[i] = 2;
    else if (pinp[i] < 3.5)   pout[i] = 3;
    else                      pout[i] = 4;
    }
    else {
    if      (pinp[i] >= -1.5) pout[i] = -1;
    else if (pinp[i] >= -2.5) pout[i] = -2;
    else if (pinp[i] >= -3.5) pout[i] = -3;
    else                      pout[i] = -4;
    }
  }

  dims[0] = nelem;

  outArray = (PyArrayObject *)
               PyArray_SimpleNewFromData(1, dims, NPY_DOUBLE, pout);
  //Py_INCREF(outArray);

  return PyArray_Return(outArray); 
} 

/* ==== methods table ====================== */
static PyMethodDef mwa_methods[] = {
  {"adc", adc, METH_VARARGS, "n-bit Analog-to-Digital Converter (ADC)"},
  {NULL, NULL, 0, NULL}
};

/* ==== Initialize ====================== */
PyMODINIT_FUNC initmwa()  {
    Py_InitModule("mwa", mwa_methods);
    import_array();  // for NumPy
}

我原以为,如果引用计数得到正确处理,Python 垃圾回收会(足够频繁地)释放输出数组占用的内存,前提是该数组名称相同且被重复使用。因此,我用以下代码对一些虚拟(但庞大)数据进行了测试:

for i in xrange(200): 
    a = rand(1000000)
    b = mwa.adc3(a)
    print i

在这里,名为“b”的数组被多次重用,它的内存从堆中借用,预计会被归还给系统。我使用gnome-system-monitor进行检查。与我的期望相反,Python拥有的内存迅速增长,并且只有通过退出程序(我使用IPython)才能释放该内存。

为了比较,我尝试使用标准NumPy函数zeros()和copy()执行相同的过程:

for i in xrange(1000): 
    a = np.zeros(10000000)
    b = np.copy(a)
    print i

如您所见,后面的代码不会引起任何内存积累。 我阅读了很多标准文档和网上的文章,尝试使用Py_INCREF(outArray)和不使用它。但是都没有成功:问题仍然存在。
然而,我在http://wiki.scipy.org/Cookbook/C_Extensions/NumPy_arrays中找到了解决方案。 作者提供了一个扩展程序matsq(),它创建一个数组并返回它。当我尝试使用作者建议的调用时:
outArray = (PyArrayObject *) PyArray_FromDims(nd,dims,NPY_DOUBLE);
pout = (double *) outArray->data;

代替我的
pout = (double *) malloc(nelem*sizeof(double));
outArray = (PyArrayObject *)
            PyArray_SimpleNewFromData(1, dims, NPY_DOUBLE, pout);
/* no matter with or without Py_INCREF(outArray)) */

内存泄漏问题解决了!程序现在正常工作。

一个问题:有人能解释为什么PyArray_SimpleNewFromData()不能提供正确的引用计数,而PyArray_FromDims()可以吗?

非常感谢。

补充说明。我可能在评论中超出了空间/时间限制,所以我在这里向Alex添加评论。我尝试通过以下方式设置OWNDATA标志:

outArray->flags |= OWNDATA;

但是我得到了“错误:‘OWNDATA’未声明”。其余内容在注释中。谢谢。
解决:正确的标志设置是
outArray->flags |= NPY_ARRAY_OWNDATA;

现在它可以工作。

Alex,抱歉。

1个回答

10
问题不在于PyArray_SimpleNewFromData,它可以生成一个正确引用计数的PyObject*。而是在于你使用的malloc,它被分配给了pout,但从未被free掉。
正如http://docs.scipy.org/doc/numpy/user/c-info.how-to-extend.html中明确说明的,文档记录了PyArray_SimpleNewFromData

ndarray不会拥有其数据。当此ndarray被释放时,指针不会被释放。 ... 如果您希望在ndarray被释放时立即释放内存,则只需在返回的ndarray上设置OWNDATA标志。

(我强调了不会)。换句话说,你正在观察到完全记录的“不会被释放”的行为,并且没有采取特别推荐的步骤,以避免该行为。

谢谢你,Alex。你的回答帮助我更好地理解了你引用文档中的短语。当然,我之前读过它,但我无法理解它的含义 - “只需在返回的ndarray上设置OWNDATA标志”。如果你知道,请告诉我。参数“requirements” - 标志的组合 - 在PyArray_SimpleNewFromData()和PyArray_Return()的参数中都不存在。我尝试这样设置它:你提到的文档页面有“数组标志”部分。 - Benkevitch
你所提到的文档页面有一个名为“数组标志”的部分。它说:“在arrayobject.h中定义了6个(二进制)标志……”,但事实并非如此。当然,他们承诺“Python提供了一个很好的基于属性的接口……用于获取(如果适当,则设置)这些标志”,但没有提供任何具体信息 :)。我该如何“简单地设置”OWNDATA标志?谢谢。 - Benkevitch
4
PyArray_ENABLEFLAGS(outArray, NPY_ARRAY_OWNDATA); 是标准的方法,但我猜你在问题的编辑中提到的方法也可以在这个版本中使用。为了未来的兼容性,我建议使用文档中记录的方法,但也许我只是一个严格遵守文档的人 :-)。那么现在可以接受我的回答吗?-) - Alex Martelli
我同意,使用文档化的方式总是更好。"现在可以接受吗?" - 可以。我不知道这是什么,我猜你是在问作为管理员关闭讨论的请求? - Benkevitch
@Benkevitch,不,我只是在问你,作为原始问题的提出者,是否可以接受这个答案(通过点击答案左侧的勾号,它会变成绿色)。 - Alex Martelli
看起来当前的文档(1.25)似乎不再解释所有这些内容了。是吗? - M. Rubio-Roy

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