您的位置:首页 > 编程语言 > C语言/C++

python的c/c++扩展内存泄露bug fix

2011-12-28 14:09 465 查看
近日需要使用到某同学使用c++封装的python扩展模块,搭配好环境,写上代码,跑起来,top一下,发现内存一直在涨。。。

于是找到源代码,大致瞅了下,发现貌似是一些引用计数的问题

py的封装最不好控制的就是引用计数了,很多时候不知道啥时候Py_INCREF,啥时候Py_DECREF,这也是为什么会存在 boost.python,pycxx的原因,诸如boost.python,pycxx的模块把这些问题都给隐藏了

ok,现在开始验证,在调用的py代码里面加上:

print len(gc.get_objects())


把程序run起来,发现数目一直在涨,目前可以确定:内存确实存在泄露。下一步,定位是哪里泄露。

看代码,发现返回结果的时候,是打包成了一个新的result结构体,把这个结构体注释掉,仅仅调用segment函数,重新编译,仍然有泄露。

看来问题不是在result结构体,研究segment的代码,发现里面的核心功能就是一个切词,把获取切词结果的函数注释掉,发现仍然有问题。

于是怀疑是使用的切词的库有问题,如果找到所使用的版本,发现这个的四位版本list中确实有个版本fix了内存泄露的问题,那么问题很明显了:使用了一个内存泄露的c库。

心中一阵狂喜,嗯嗯,问题应该可以解决了。

于是更新所使用的c库,重新build出so,跑上py代码,top下观看内存,咦,怎么内存还是一直在涨而object不变?

很奇怪,object数目不变,就表示没有内存泄露,但是内存却一直在涨。。。

怀疑是代码写的不对,于是改用了两种方法来做:

1,把写的c扩展的代码,改用类实现

2,写个c的简单的封装,调用py的ctypes来使用这个so

写完后验证了下,发现object仍然不变,但内存一直在涨。这说明:

1,内存没泄露---否则object数目会一直增长

2,很可能是py自己获取了内存后,就一直未释放(这里的未释放不是指不会释放该对象,而是指不会释放管理这个对象的object指针)

google了下,发现很多人都曾验证过,py确实如此,比如 这里 ,另外,这里 也有一篇文章说为什么py不释放内存。

于是想,好了,事情到此为止了,好歹有个交代了。于是准备放弃研究这个问题,把其归为py自己的内存管理的问题:py自己的pool中获取到内存就不会释放。

原本以为事情就到此可以结束,中午吃饭和同事聊到这个问题,他说可以参考下py的iterator的方法呢?

嗯,主意不错,我用iterator重新改写下这个c的扩展

于是在
这里 发现了一个参考的用c写的iterator的实现,代码很好理解

然后我打算实现下,开始修改代码,咦等等,代码中返回结构体的时候,有个PyString_FromString,会不是是这个函数有问题?

跟进去看了下PyString_FromString的实现:

PyObject *
PyString_FromString(const char *str)
{
register size_t size;
register PyStringObject *op;

assert(str != NULL);
size = strlen(str);
if (size > PY_SSIZE_T_MAX - PyStringObject_SIZE) {
PyErr_SetString(PyExc_OverflowError,
"string is too long for a Python string");
return NULL;
}
if (size == 0 && (op = nullstring) != NULL) {
#ifdef COUNT_ALLOCS
null_strings++;
#endif
Py_INCREF(op);
return (PyObject *)op;
}
if (size == 1 && (op = characters[*str & UCHAR_MAX]) != NULL) {
#ifdef COUNT_ALLOCS
one_strings++;
#endif
Py_INCREF(op);
return (PyObject *)op;
}

/* Inline PyObject_NewVar */
op = (PyStringObject *)PyObject_MALLOC(PyStringObject_SIZE + size);
if (op == NULL)
return PyErr_NoMemory();
PyObject_INIT_VAR(op, &PyString_Type, size);
op->ob_shash = -1;
op->ob_sstate = SSTATE_NOT_INTERNED;
Py_MEMCPY(op->ob_sval, str, size+1);
/* share short strings */
if (size == 0) {
PyObject *t = (PyObject *)op;
PyString_InternInPlace(&t);
op = (PyStringObject *)t;
nullstring = op;
Py_INCREF(op);
} else if (size == 1) {
PyObject *t = (PyObject *)op;
PyString_InternInPlace(&t);
op = (PyStringObject *)t;
characters[*str & UCHAR_MAX] = op;
Py_INCREF(op);
}
return (PyObject *) op;
}


啧啧,每个if分支里面都有处理引用计数,没有问题啊。。。

那莫不是函数PyList_Append有问题?

google了下,发现PyList_Append确实会增加待添加的这个临时对象的引用计数,于是看了下PyList_Append的实现代码:

int
PyList_Append(PyObject *op, PyObject *newitem)
{
if (PyList_Check(op) && (newitem != NULL))
return app1((PyListObject *)op, newitem);
PyErr_BadInternalCall();
return -1;
}
核心的功能在app1函数中做的,于是跟进去看看:

static int
app1(PyListObject *self, PyObject *v)
{
Py_ssize_t n = PyList_GET_SIZE(self);

assert (v != NULL);
if (n == PY_SSIZE_T_MAX) {
PyErr_SetString(PyExc_OverflowError,
"cannot add more objects to list");
return -1;
}

if (list_resize(self, n+1) == -1)
return -1;

Py_INCREF(v);
PyList_SET_ITEM(self, n, v);
return 0;
}
呃,原来在这里面有重新增加了临时对象的引用计数。。。重新增加了。。

好了,问题明朗了,这个应该算是py的bug了么?appen的时候会重新增加引用计数,看看setitem的实现:

int
PyList_SetItem(register PyObject *op, register Py_ssize_t i,
register PyObject *newitem)
{
register PyObject *olditem;
register PyObject **p;
if (!PyList_Check(op)) {
Py_XDECREF(newitem);
PyErr_BadInternalCall();
return -1;
}
if (i < 0 || i >= Py_SIZE(op)) {
Py_XDECREF(newitem);
PyErr_SetString(PyExc_IndexError,
"list assignment index out of range");
return -1;
}
p = ((PyListObject *)op) -> ob_item + i;
olditem = *p;
*p = newitem;
Py_XDECREF(olditem);
return 0;
}


嗯,这个实现很好,没有增加对待插入的对象的引用计数

修改代码,把pylist_append改用pylist_setitem,重新build出so,跑起代码,top看下内存,很好,内存不再增长。

google了下,发现这个bug已经有人报过,可惜url被墙,google的快照倒是可以看到,摘录如下:

PyList_Append increments the reference count of the item being appended to the list, and two of the places in the
GDB Python code that use it don't take that into account. The result is a memory leak.
The attached patch fixes that.

Tested by using a debug build of Python with reference counting enabled, and turning on dumping of final leftover objects.

代码的bug是在2011年9月6号commit进去的,我所使用的python版本的发布日期如下:

Python 2.7.2 was released on June 11th, 2011.

所以,嗯,是的,这个bug还未发布,好吧,那就先使用pylist_setitem吧。

到此为止,问题全部解决。

罗嗦几句:

1,使用google code search到的代码,里面很多成熟的开源的扩展,比如py的matlab扩展包,都是使用的pylist_append,难道他们测试的时候就没发现内存持续增长吗??这么看来,如此成熟的开源的东西也不一定可靠。尽信书不如无书,两个一样的道理。

2,不建议直接用c写扩展,比较难把握,出问题的时候没有好的调试工具,建议:简单的代码直接使用ctype,复杂的代码可以使用c来简单封装下,然后写成so给py用ctype调用。

3,py的内存泄露不好调试,valgrind可以检测,但是信息不多,这里 有个图形化的工具,原理是打印出所有object信息,然后用画图工具画出来各个对象的关系,觉得比较麻烦,未曾使用
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: