#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.