内存泄漏,其中 CPython 扩展向 Python 返回一个“PyList_New”实例,该实例永远不会被释放

Posted

技术标签:

【中文标题】内存泄漏,其中 CPython 扩展向 Python 返回一个“PyList_New”实例,该实例永远不会被释放【英文标题】:Memory leak where CPython extension returns a 'PyList_New' instance to Python, which is never deallocated 【发布时间】:2022-01-13 19:54:29 【问题描述】:

几天来我一直在尝试调试内存泄漏,但我的想法已经不多了。

高级:我编写了一个 CPython 扩展,允许查询二进制数据文件,并将结果作为 Python 对象列表返回。用法类似于这个伪代码:

for config in configurations:
    s = Strategy(config)
    for date in alldates:
        data = extension.getData(date)
        # do analysis on 'data', capture/save statistics

我使用了 tracemalloc、memory_profiler、objgraph、sys.getrefcount 和 gc.get_referrers 来尝试找到根本原因,这些工具都将这个扩展作为大量内存(许多 gigs)的来源。就上下文而言,二进制文件中的单个记录为 64 字节,通常每天有 390 条记录,因此每次 date 迭代使用约 24K 字节。现在,发生了许多迭代(同步),但在每次迭代中,data 被用作局部变量,所以我希望每个后续分配都释放前一个对象。 memory_profile 的结果表明并非如此......

Line #    Mem usage    Increment  Occurences   Line Contents
============================================================
    86     33.7 MiB     33.7 MiB           1    @profile
    87                                          def evaluate(self, date: int, filterConfidence: bool, limitToMaxPositions: bool, verbose: bool) -> None:
    92    112.7 MiB      0.0 MiB         101        for symbol in self.symbols:
    93    111.7 MiB      0.0 MiB         100            fromdate: int = TradingDays.getAdjacentDay(date, -(self.config.analysisPeriod - 1))
    94    111.7 MiB      0.0 MiB         100            throughdate: int = date
    95                                                  
    96    111.7 MiB      0.0 MiB         100            maxtime: int = self.config.maxTimeToGain
    97    111.7 MiB      0.0 MiB         100            target: float = self.config.profitTarget
    98    111.7 MiB      0.0 MiB         100            islong: bool = self.config.isLongStrategy
    99                                                      
   100    111.7 MiB      0.8 MiB         100            avgtime: Optional[int] = FileStore.getAverageTime(symbol, maxtime, target, islong, fromdate, throughdate, verbose)
   101    111.7 MiB      0.0 MiB         100            if avgtime is None:
   102    110.7 MiB      0.0 MiB          11                continue
   103                                                      
   104    112.7 MiB     78.3 MiB          89            weightedModel: WeightedModel = self.testAverageTimes(symbol, avgtime, fromdate, throughdate)
   105    112.7 MiB      0.0 MiB          89            if weightedModel is not None:
   106    112.7 MiB      0.0 MiB          88                self.watchlist.append(weightedModel)
   107    112.7 MiB      0.0 MiB          88                self.averageTimes[symbol] = avgtime
   108                                                      
   109    112.7 MiB      0.0 MiB           1        if verbose:
   110                                                  print('\nFull Evaluation Results')
   111                                                  print(self.getWatchlistTableString())
   112                                         
   113    112.7 MiB      0.0 MiB           1        self.watchlist.sort(key=WeightedModel.sortKey, reverse=True)
   114                                         
   115    112.7 MiB      0.0 MiB           1        if filterConfidence:
   116    112.7 MiB      0.0 MiB          91            self.watchlist = [ m for m in self.watchlist if m.getConfidence() >= self.config.winRate ]
   117                                                  
   118    112.7 MiB      0.0 MiB           1        if limitToMaxPositions:
   119                                                  self.watchlist = self.watchlist[:self.config.maxPositions]
   120                                         
   121    112.7 MiB      0.0 MiB           1        return

这是来自evaluate 函数的第一次迭代(总共有 30 次迭代)。第 104 行似乎是在积累内存。奇怪的是weightedModel 仅包含有关查询数据的基本统计信息,并且该数据存储在循环局部变量中。我不明白为什么每次内部迭代后都没有清理所使用的内存。

我尝试在迭代完成后del 有问题的对象,但没有效果。包含对象的引用计数确实很高,并且 gc.get_referrers 将对象显示为引用自身 (?)。

我很乐意提供额外的信息/代码,但我已经尝试了很多事情,此时头脑转储将是一团糟:) 我希望有更多经验的人能够帮助我集中精力思考过程。

干杯!

【问题讨论】:

【参考方案1】:

找到了!泄漏更深一层,扩展函数在其中构建了一个 Python 对象的实例。

这是泄露的版本:

PyObject* obj = PyObject_CallObject(PRICEBAR_CLASS_DEF, args);

PyObject_SetAttrString(obj, "id", PyLong_FromLong(bar->id));
# a bunch of other attrs...

return obj;

这是固定版本:

PyObject* obj = PyObject_CallObject(PRICEBAR_CLASS_DEF, args);

PyObject* id = PyLong_FromLong(bar->id);
# others...

PyObject_SetAttrString(obj, "id", id);
# others...

Py_DECREF(id);
# others...

return obj;

出于某种原因,我想到 PyLong_FromLong 函数没有增加结果对象的引用计数,但这显然不是真的。这就是我为每个创建的 bar 对象增加一个额外的引用计数的方式。

【讨论】:

以上是关于内存泄漏,其中 CPython 扩展向 Python 返回一个“PyList_New”实例,该实例永远不会被释放的主要内容,如果未能解决你的问题,请参考以下文章

键盘扩展内存泄漏?

使用 C 扩展 python 时发现内存泄漏

SGI STL内存配置器:内存泄漏?

请解释一下“内存泄漏”,这个问题会有啥影响

Cpython和Jython的对比介绍

如何检测 iPhone 上的内存泄漏?