使用 PyArray_SimpleNewFromData() 创建并返回数组时 Python 扩展中的内存泄漏

Memory leak in Python extension when array is created with PyArray_SimpleNewFromData() and returned

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

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" 的数组被多次重复使用,其内存由 adc3() 从堆中借用,预计将 returned 到系统。我用 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(),它创建一个数组并returns它。当我尝试使用作者建议的调用时:

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() 可以?

非常感谢。

添加。我的评论可能超过了 room/time,所以我在这里添加对 Alex 的评论。 我试图以这种方式设置 OWNDATA 标志:

outArray->flags |= OWNDATA;

但我得到了 "error: ‘OWNDATA’ undeclared"。 剩下的在评论里。提前谢谢你。


已解决:标志的正确设置是

outArray->flags |= NPY_ARRAY_OWNDATA;

现在可以了。

亚历克斯,对不起。

问题是 而不是 PyArray_SimpleNewFromData 会产生正确的重新计数 PyObject*。相反,它与您的 malloc 一起分配给 pout 然后 never freed.

正如 http://docs.scipy.org/doc/numpy/user/c-info.how-to-extend.html 上的文档明确指出的那样,记录 PyArray_SimpleNewFromData:

the ndarray will not own its data. When this ndarray is deallocated, the pointer will not be freed. ... If you want the memory to be freed as soon as the ndarray is deallocated then simply set the OWNDATA flag on the returned ndarray.

(我强调的是而不是)。 IOW,您正在观察如此清楚记录的 "will not be freed" 行为,如果您想避免上述行为,则没有采取特别推荐的步骤。