При оценке памяти объекта через sys.getsizeof получилась неожиданно малая цифра: какой принцип измерения нужно проверить первым?
sys.getsizeof измеряет только непосредственный размер переданного объекта, а не полный размер всего графа объектов, доступных через него. Для контейнера это обычно не включает размер его элементов, поэтому оценка может оказаться заниженной; при наивном рекурсивном подсчёте, наоборот, общие объекты можно посчитать несколько раз.
Python предоставляет размер объекта на уровне объектной модели, чтобы быстро получать размер, непосредственно связанный с конкретным объектом. Такой подход подходит для локальной диагностики и не требует обходить произвольный граф ссылок.
Полный размер объекта неоднозначен: объекты могут совместно использовать одни и те же значения, образовывать циклы или ссылаться на память, которой управляет библиотека на C-уровне. Поэтому универсальная функция не может просто сложить размеры всех достижимых объектов и получить точное потребление процесса.
Контейнер может занимать немного памяти сам по себе, но удерживать большой объём данных в элементах. Если измерять только контейнер, причина роста памяти останется незамеченной.
Обратная ошибка возникает при рекурсивном обходе без множества уже посещённых объектов: одна строка, буфер или другой объект, на который ссылаются несколько элементов, будет учтён многократно. Циклические ссылки могут дополнительно привести к бесконечному обходу.
Для приближённой оценки графа нужно начинать с корневого объекта, рекурсивно обходить его референты и учитывать каждый объект только один раз по его идентификатору. Это измеряет размер Python-объектов, достижимых из корня, но не является точным измерением RSS процесса или полного объёма памяти приложения.
В примере root содержит две ссылки на один и тот же bytearray. Переменная naive учитывает буфер дважды, а unique — один раз. При этом sys.getsizeof(root) сам по себе не включает размер buffer.
На практике нужно заранее определить, что именно измеряется: размер графа объектов, объём памяти, удерживаемый конкретным объектом, или вклад компонента в память процесса. Для поиска источника Python-аллокаций применяют tracemalloc, а для более широкого анализа процесса нужны профилировщики, учитывающие память вне Python-объектов.
sys.getsizeof использует размер, предоставленный объектом через механизм __sizeof__; для отслеживаемых сборщиком объектов учитывается также соответствующая служебная часть. Реализация пользовательского типа может не включить в этот размер внешние буферы, память C-библиотеки, отображённые файлы или ресурсы стороннего расширения.
Полный рекурсивный подсчёт также имеет ограничения. Он показывает размер объектов в графе ссылок, но не отвечает автоматически на вопрос, кто фактически владеет общей памятью и что освободится после удаления конкретной ссылки.
В сервисе с кэшем разработчик измерил размер словаря через sys.getsizeof и решил, что кэш занимает мало памяти. На самом деле словарь содержал крупные ответы, а некоторые строки и буферы были общими для множества записей.
Рассматривались три варианта:
Выбрали третий вариант: оценку графа использовали для анализа структуры кэша, а tracemalloc — для поиска мест выделения Python-памяти. Это позволило отличить крупные значения, реально удерживаемые кэшем, от повторных ссылок и не делать вывод о памяти процесса только по одному числу sys.getsizeof.
Вопрос: Можно ли сложить размеры всех объектов, достижимых из корня, чтобы получить объём памяти, который освободится после удаления корня?
Ответ: Нет. Часть объектов может быть доступна из других корней: глобальных структур, других кэшей или фоновых задач. Такой обход показывает размер достижимого графа, но не уникальный вклад корня в освобождаемую память. Для этого требуется анализ владельцев и альтернативных путей достижимости.
Вопрос: Почему размер объекта по sys.getsizeof может не включать значительную часть памяти расширения Python?
Ответ: Расширение может хранить данные во внешнем буфере, в структурах библиотеки C или в памяти, не представленной отдельными Python-объектами. Если реализация не включает этот ресурс в __sizeof__, значение будет отражать только оболочку Python. Поэтому для таких компонентов нужно использовать документацию библиотеки и инструменты, которые видят нативные выделения.
Вопрос: Что изменится, если два разных контейнера ссылаются на один большой объект?
Ответ: При подсчёте размера каждого контейнера отдельно общий объект может попасть в обе оценки, хотя в памяти существует один экземпляр. Для оценки объединённого графа нужен общий набор посещённых идентификаторов; для сравнения владельцев дополнительно требуется анализ путей ссылок, потому что уникальный размер и размер удерживаемого графа — разные показатели.