Программирование PythonПамять и производительностьИнженер по производительности Python

В сценарии с множеством небольших временных объектов RSS процесса остаётся высоким после их удаления: как а...

В сценарии с множеством небольших временных объектов RSS процесса остаётся высоким после их удаления: как аллокатор малых объектов CPython объясняет такое поведение?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

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

Исторический контекст

Аллокатор малых объектов появился для снижения стоимости частых выделений и освобождений, характерных для Python. Передача каждого небольшого запроса системному malloc была бы дороже и могла бы приводить к дополнительной фрагментации.

pymalloc использует собственные структуры фиксированных классов размеров и получает у системного аллокатора более крупные области памяти. Это ускоряет типичные операции с объектами Python, но отделяет освобождение объекта от немедленного возврата памяти операционной системе.

Постановка проблемы

Предположим, программа по очереди создаёт множество небольших объектов разных размеров. После удаления объектов свободное место может оказаться распределено по разным пулам внутри арен, а не сосредоточено в полностью свободных аренах.

В результате внутренний объём, доступный для будущих выделений, уменьшается, но RSS процесса почти не меняется. Ошибочно считать такой RSS доказательством того, что все удалённые объекты всё ещё достижимы или что сборщик мусора не работает.

Подробное решение

Для малых блоков pymalloc использует иерархию «арена — пул — блок». Пулы обслуживают определённые классы размеров, а арены содержат множество пулов. Освобождение объекта возвращает его блок в соответствующий пул, где он может быть выдан следующему объекту того же класса.

Арена может быть отдана системе только при достаточной степени освобождения, обычно когда все её пулы больше не нужны. Если в арене остаётся хотя бы несколько занятых блоков, остальные свободные блоки могут быть недоступны операционной системе как единая возвращаемая область.

Ситуация зависит от размера объектов и конкретного аллокатора. Крупные выделения могут идти в обход pymalloc, а поведение системного malloc, используемой версии CPython и платформы различается. Поэтому нельзя по одному RSS определить наличие утечки.

Диагностику нужно разделять по уровням: проверять наличие живых объектов инструментами вроде tracemalloc, наблюдать RSS процесса и учитывать, что эти измерения описывают разные слои памяти. Если живые Python-объекты не растут, а RSS стабильно выше ожидаемого, вероятны удержание памяти аллокатором, фрагментация, нативные буферы или память библиотек расширений.

Практические меры — уменьшать количество краткоживущих объектов, повторно использовать буферы, выбирать компактные структуры данных и устранять резкие чередования выделений разных размеров. Принудительное сведение арены или возврат памяти ОС не является общей гарантией CPython; смена аллокатора может помочь в конкретной нагрузке, но требует измерений и способна изменить производительность.

Ситуация из практики

Сервис разбирал большие партии сообщений и для каждого сообщения создавал временные небольшие словари, списки и строки. После завершения партии число живых объектов возвращалось к базовому уровню, но RSS постепенно рос и затем стабилизировался на высоком значении.

Рассматривались три варианта. Увеличить частоту сборки циклического мусора было малоэффективно: большинство объектов освобождалось подсчётом ссылок, а проблема находилась на уровне размещения уже свободных блоков. Полностью заменить структуры на крупные внешние буферы уменьшало число объектов, но усложняло код и повышало риск ошибок управления временем жизни. Перезапуск рабочих процессов освобождал память ОС, но добавлял стоимость перезапуска и усложнял эксплуатацию.

Выбранным решением стало уменьшить количество временных объектов и переиспользовать буферы в горячем пути, после чего отдельно проверить RSS, tracemalloc и число живых объектов. Такой подход адресует причину фрагментации и снижает аллокационную нагрузку, не полагаясь на то, что каждый del немедленно уменьшит RSS.

Что кандидаты часто упускают

  1. Дополнительный вопрос: Всегда ли высокий RSS после удаления малых объектов означает фрагментацию pymalloc?

Ответ: Нет. Причиной могут быть живые ссылки, циклы до запуска сборщика, память расширений C, буферы библиотек, отображённые файлы или особенности системного аллокатора. Сначала нужно сопоставить граф живых объектов и распределение Python-выделений с RSS; вывод о фрагментации допустим только после исключения удержания объектов и нативной памяти.

  1. Дополнительный вопрос: Почему уменьшение числа живых объектов может не уменьшить RSS, но всё же ускорить последующие операции?

Ответ: Освобождённые блоки остаются в пулах и могут быть повторно выданы без нового обращения к операционной системе. Поэтому внутренний кэш аллокатора снижает стоимость будущих выделений, хотя внешний размер процесса не изменяется. Это компромисс между быстрым повторным использованием памяти и её возвратом ОС.

  1. Дополнительный вопрос: Почему одинаковое число объектов не гарантирует одинаковый объём удерживаемой аллокатором памяти?

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