Программирование PythonПамять и производительностьPython-разработчик производительных сервисов

При оценке памяти объекта через sys.getsizeof получилась неожиданно малая цифра: какой принцип измерения ну...

При оценке памяти объекта через sys.getsizeof получилась неожиданно малая цифра: какой принцип измерения нужно проверить первым?

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

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

sys.getsizeof измеряет только непосредственный размер переданного объекта, а не полный размер всего графа объектов, доступных через него. Для контейнера это обычно не включает размер его элементов, поэтому оценка может оказаться заниженной; при наивном рекурсивном подсчёте, наоборот, общие объекты можно посчитать несколько раз.

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

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

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

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

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

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

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

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

import sys buffer = bytearray(1_000_000) root = [buffer, buffer] shallow = sys.getsizeof(root) naive = shallow + sum(sys.getsizeof(item) for item in root) unique = shallow + sys.getsizeof(buffer) print(shallow, naive, unique)

В примере root содержит две ссылки на один и тот же bytearray. Переменная naive учитывает буфер дважды, а unique — один раз. При этом sys.getsizeof(root) сам по себе не включает размер buffer.

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

sys.getsizeof использует размер, предоставленный объектом через механизм __sizeof__; для отслеживаемых сборщиком объектов учитывается также соответствующая служебная часть. Реализация пользовательского типа может не включить в этот размер внешние буферы, память C-библиотеки, отображённые файлы или ресурсы стороннего расширения.

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

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

В сервисе с кэшем разработчик измерил размер словаря через sys.getsizeof и решил, что кэш занимает мало памяти. На самом деле словарь содержал крупные ответы, а некоторые строки и буферы были общими для множества записей.

Рассматривались три варианта:

  • складывать размеры словаря и всех элементов без проверки повторов — просто, но общие объекты считаются многократно;
  • рекурсивно обходить граф без учёта посещённых объектов — потенциально приводит к двойному подсчёту и проблемам на циклах;
  • выполнять обход с множеством идентификаторов и отдельно сопоставлять результат с данными профилировщика — сложнее, зато устраняет двойной подсчёт и позволяет проверить интерпретацию.

Выбрали третий вариант: оценку графа использовали для анализа структуры кэша, а tracemalloc — для поиска мест выделения Python-памяти. Это позволило отличить крупные значения, реально удерживаемые кэшем, от повторных ссылок и не делать вывод о памяти процесса только по одному числу sys.getsizeof.

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

  1. Вопрос: Можно ли сложить размеры всех объектов, достижимых из корня, чтобы получить объём памяти, который освободится после удаления корня?

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

  2. Вопрос: Почему размер объекта по sys.getsizeof может не включать значительную часть памяти расширения Python?

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

  3. Вопрос: Что изменится, если два разных контейнера ссылаются на один большой объект?

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