You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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%,推测与垃圾回收的非确定性有关,但缺乏合理解释。

尝试的修复方案

  1. 调用Py_DECREF(p_result)和Py_DECREF(p_list),但内存泄漏问题未彻底解决;
  2. 通过Py_SET_REFCNT()将p_result元素的引用计数设为0,初期有效,但结合Py_DECREF(p_result)使用时,会在PyObject_CallOneArg()处触发段错误,且try{}catch{}块无法捕获该错误。

待解决问题

  1. 为何对p_result元素调用Py_SET_REFCNT()会导致后续段错误?
  2. 为何代码无法捕获该段错误?
  3. 最终应如何解决内存溢出问题?
    此外,希望获得调试建议,例如无法使用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)

  1. Python内置内存分析工具

    • 使用tracemalloc模块跟踪内存分配:在Python脚本开头加入import tracemalloc; tracemalloc.start(),结束时打印内存快照,定位未回收的对象;
    • 使用gc.get_objects()查看当前存活的所有Python对象,检查是否有大量重复的字符串、张量堆积。
  2. C++端跟踪引用计数

    • 在关键位置调用Py_REFCNT(obj)打印对象引用计数,观察计数变化是否符合预期(如创建后计数为1,插入列表后计数不变);
    • 使用PyDebug_SetTrace设置Python级调试钩子,跟踪对象的创建与销毁过程。
  3. 系统级内存监控

    • 使用Linux下的top/htop或Windows任务管理器,观察内存增长节奏,判断泄漏发生在C++端还是Python端;
    • 使用pmap工具查看进程内存映射,定位持续增长的内存区域。

内容的提问来源于stack exchange,提问作者RBaumgar

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 20:12:02