Утечка памяти, при которой расширение CPython возвращает экземпляр ‘PyList_New’ в Python, который никогда не освобождается

#memory-leaks #python-3.8 #cpython

#утечки памяти #python-3.8 #cpython

Вопрос:

Я уже несколько дней пытаюсь отладить утечку памяти, и у меня заканчиваются идеи.

Высокий уровень: я написал расширение CPython, которое позволяет выполнять запросы к двоичным файлам данных, и оно возвращает результаты в виде списка объектов Python. Использование аналогично этому psuedocode:

 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, чтобы попытаться найти основную причину, и все эти инструменты указывают на это расширение как источник непомерного объема памяти (много гигов). Для контекста одна запись в двоичном файле составляет 64 байта, обычно в день записывается 390 записей, поэтому каждая date итерация работает с ~24 КБ байтов. Теперь происходит много итераций (синхронно), но в каждой итерации 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) -gt; 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() gt;= 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-gt;id)); # a bunch of other attrs...  return obj;  

Это исправленная версия:

 PyObject* obj = PyObject_CallObject(PRICEBAR_CLASS_DEF, args);  PyObject* id = PyLong_FromLong(bar-gt;id); # others...  PyObject_SetAttrString(obj, "id", id); # others...  Py_DECREF(id); # others...  return obj;  

По какой-то причине у меня в голове было, что функция PyLong_FromLong не увеличивала количество ссылок для результирующего объекта, но это, по-видимому, не так. Вот как я получил дополнительное количество ссылок для каждого созданного объекта bar.