内存泄漏,其中 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”实例,该实例永远不会被释放的主要内容,如果未能解决你的问题,请参考以下文章