CPython垃圾回收与引用计数问题及C++调用Python内存泄漏排查
问题背景
我有一个C程序,需要运行神经网络。为便于使用,C代码先编译一批网络输入,再调用Python脚本,Python脚本返回列表后由C++接收。相关函数代码如下:
void strings_to_pylist(PyObject* p_list_out, const vector<string>& c_list) const { int i = 0; for (const string& c_symbol : c_list) { PyObject* p_symbol = PyUnicode_FromString(c_symbol.c_str()); PyList_SetItem(p_list_out, i, p_symbol); ++i; } } const vector< pair<int, float> > my_func(const vector< vector<string> >& query_traces, data_map& id) const { PyObject* p_list = PyList_New(query_traces.size()); for(int i=0; i<query_traces.size(); i++){ PyObject* p_tmp = PyList_New(query_traces[i].size()); strings_to_pylist(p_tmp, query_traces[i]); PyList_SetItem(p_list, i, p_tmp); } PyObject* p_result; try{ p_result = PyObject_CallOneArg(query_func, p_list); } catch(...){ cout << "Running gc and trying again" << endl; PyRun_SimpleString("gc.collect()"); p_result = PyObject_CallOneArg(query_func, p_list); } if (!PyList_Check(p_result)) throw std::runtime_error("Something went wrong, the Network did not return a list. What happened?"); vector< pair<int, float> > res; for(int i=0; i<query_traces.size(); i++){ PyObject* p_type = PyList_GetItem(p_result, static_cast<Py_ssize_t>(i*2)); if(!PyUnicode_CheckExact(p_type)){ cerr << "Problem with type as returned by Python script. Is it a proper int?" << endl; throw exception(); // force the catch block } PyObject* p_confidence = PyList_GetItem(p_result, static_cast<Py_ssize_t>(i*2 + 1)); if(!PyFloat_CheckExact(p_confidence)){ cerr << "Problem with type as returned by Python script. Is it a proper float?" << endl; throw exception(); // force the catch block } int type = id.get_reverse_type(PyUnicode_AsUTF8(p_type)); if(type > id.get_alphabet_size()){ id.add_type(PyUnicode_AsUTF8(p_type)); } res.emplace_back(type, static_cast<float>(PyFloat_AsDouble(p_confidence))); //Py_SET_REFCNT(p_type, 0); //Py_SET_REFCNT(p_confidence, 0); } //Py_DECREF(p_list); //Py_DECREF(p_result); return res; }
注:原代码中PyList_SetItem(p_list, idx, p_item)(p_list_out, i, p_symbol);为笔误,已修正为PyList_SetItem(p_list_out, i, p_symbol);
内存泄漏现象
strings_to_pylist中的p_symbol引用计数极高(远超1000),而列表自身引用计数始终为1,无论插入前后;p_result的引用计数为1,但其中元素的引用计数同样远超1000;- 每次运行内存增长情况不同,固定步数后内存占用有时达60%,有时仅30%,推测与垃圾回收的非确定性有关,但缺乏合理解释。
尝试的修复方案
- 调用
Py_DECREF(p_result)和Py_DECREF(p_list),但内存泄漏问题未彻底解决; - 通过
Py_SET_REFCNT()将p_result元素的引用计数设为0,初期有效,但结合Py_DECREF(p_result)使用时,会在PyObject_CallOneArg()处触发段错误,且try{}catch{}块无法捕获该错误。
待解决问题
- 为何对
p_result元素调用Py_SET_REFCNT()会导致后续段错误? - 为何代码无法捕获该段错误?
- 最终应如何解决内存溢出问题?
此外,希望获得调试建议,例如无法使用valgrind工具调试Python执行部分的替代方案。
解答
问题1:Py_SET_REFCNT()导致段错误的原因
Py_SET_REFCNT()是Python底层的引用计数修改宏,完全绕过了Python的引用计数管理逻辑:
PyList_GetItem()返回的是列表元素的借用引用,你没有所有权,不能直接修改其引用计数;- 当你将元素引用计数强制设为0,后续
Py_DECREF(p_result)释放列表时,Python会认为这些元素已无引用,直接释放内存。但这些元素可能还被Python解释器内部对象(如神经网络框架缓存、全局变量、垃圾回收链表)引用,后续代码访问已释放的内存就会触发段错误。
问题2:无法捕获段错误的原因
C的try/catch仅能捕获**C异常**,而段错误属于操作系统级信号错误(如SIGSEGV),是内存非法访问触发的硬件异常,不属于C++异常体系,因此catch(...)无法捕获这类错误。若要处理段错误,需使用信号处理函数(如signal(SIGSEGV, handler)),但通常仅用于优雅退出,无法恢复程序执行。
问题3:彻底解决内存溢出的方案
1. 修正引用计数管理的核心错误
输入列表的引用计数处理
PyList_New()创建的列表引用计数为1,PyList_SetItem()会窃取传入对象的引用(无需手动Py_DECREF传入元素),这部分逻辑是正确的;- 原代码注释掉了
Py_DECREF(p_list),导致每次调用my_func都会泄漏整个输入列表的内存,这是核心泄漏点之一,必须恢复调用。
结果列表的引用计数处理
PyObject_CallOneArg()返回的p_result是拥有引用,使用完毕后必须调用Py_DECREF(p_result)释放;- 注意:
PyList_GetItem()返回的是借用引用,禁止对其调用Py_DECREF,否则会导致引用计数异常。
2. 辅助:主动触发Python垃圾回收
在释放p_result和p_list后,可主动调用Python垃圾回收,清理可能存在的循环引用:
PyRun_SimpleString("import gc; gc.collect()");
3. 排查Python脚本端的泄漏
若C++端引用计数处理正确仍有泄漏,需检查Python脚本:
- 确认神经网络框架(如TensorFlow/PyTorch)的缓存、中间张量是否已清理;
- 确保返回列表的元素未被全局对象引用,否则即使C++释放了
p_result,这些元素也无法被回收。
修正后的核心代码片段
// 函数末尾添加引用释放 Py_DECREF(p_list); Py_DECREF(p_result); // 主动触发垃圾回收 PyRun_SimpleString("import gc; gc.collect()"); return res;
调试建议(替代valgrind)
Python内置内存分析工具
- 使用
tracemalloc模块跟踪内存分配:在Python脚本开头加入import tracemalloc; tracemalloc.start(),结束时打印内存快照,定位未回收的对象; - 使用
gc.get_objects()查看当前存活的所有Python对象,检查是否有大量重复的字符串、张量堆积。
- 使用
C++端跟踪引用计数
- 在关键位置调用
Py_REFCNT(obj)打印对象引用计数,观察计数变化是否符合预期(如创建后计数为1,插入列表后计数不变); - 使用
PyDebug_SetTrace设置Python级调试钩子,跟踪对象的创建与销毁过程。
- 在关键位置调用
系统级内存监控
- 使用Linux下的
top/htop或Windows任务管理器,观察内存增长节奏,判断泄漏发生在C++端还是Python端; - 使用
pmap工具查看进程内存映射,定位持续增长的内存区域。
- 使用Linux下的
内容的提问来源于stack exchange,提问作者RBaumgar
相关产品推荐
相关产品推荐

