Хабрахабр

Объекты без циклических ссылок и циклической сборки мусора

Это увеличивает след в памяти каждого экземпляра и может создавать проблемы с памятью в высоконагруженных системах. Каждый экземпляр класса в CPython, созданный при помощи синтаксической конструкции class, участвует в механизме циклической сборки мусора.

Нельзя ли обойтись в случае необходимости одним базовым механизмом подсчета ссылок?

Разберем один подход, который поможет создавать классы, чьи экземпляры будут удаляться только с помощью механизма подсчета ссылок.

Немного о механизме сборки мусора в CPython

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

lst = [] lst.append(lst) del lst

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

Накладные расходы, связанные с механизмом циклической сборки мусора

Но с ним связаны определенные накладные расходы: Обычно механизм циклической сборки мусора не создает проблем.

7 и не менее 16 байтов в 3. К каждому экземпляру класса при распределении памяти добавляется заголовок PyGC_Head: (не менее 24 байта на Python <= 3. 8 на 64-битной платформе.

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

Можно ли ограничиться иногда базовым механизмом подсчета ссылок?

Например, записи, содержащие значения простого типа (числа, строки, дата/время). Механизм циклической сборки мусора может оказаться избыточным, когда класс представляет нерекурсивный тип данных. Для иллюстрации рассмотрим простой класс:

class Point: x: int y: int

Хотя в Python ничто не мешает "выстрелить себе в ногу": При корректном его использовании циклы ссылок невозможны.

p = Point(0, 0) p.x = p

Но стандартного механизма отказа от циклической сборки мусора для отдельно взятого класса пока нет. Тем не менее для класса Point как раз можно было бы ограничиться механизмом подсчета ссылок.

Он определяет, что экземпляры класса будут включены в механизм циклического сборки мусора. Современный CPython устроен так, что при определении пользовательских классов в структуре, отвечающей за тип, который определяет пользовательский класс, всегда устанавливается флаг Py_TPFLAGS_HAVE_GC. Если флаг Py_TPFLAGS_HAVE_GC не установлен, то работает только базовый механизм подсчета ссылок. Для всех таких объектов при его создании добавляется заголовок PyGC_Head, осуществляется их включение в список отслеживаемых объектов. Потребуется внести изменения в ядро CPython, ответственное за создание и разрушение экземпляров. Однако, одним сбросом Py_TPFLAGS_HAVE_GC обойтись не получится. А это пока проблематично.

Об одной реализации

С его помощью можно создавать классы, экземпляры которых не участвуют в механизме циклической сборки мусора (Py_TPFLAGS_HAVE_GC не установлен и, соответственно, нет дополнительного заголовка PyGC_Head). В качестве примера реализации идеи рассмотрим базовый класс dataobject из проекта recordclass. Они имеют точно такую же структуру в памяти, как и экземпляры классов со __slots__, но без PyGC_Head:

from recordclass import dataobject
class Point(dataobject): x:int y:int >>> p = Point(1,2)
>>> print(p.__sizeof__(), sys.getsizeof(p))
32 32

Для сравнения приведем аналогичный класс со __slots__:

class Point: __slots__ = 'x', 'y' x:int y:int >>> p = Point(1,2)
>>> print(p.__sizeof__(), sys.getsizeof(p))
32 64

Для экземпляров с несколькими атрибутами такое увеличение размера его следа в оперативной памяти может оказаться существенным. Разница в размерах как раз равна размеру заголовка PyGC_Head. Для экземпляров класса Point добавление PyGC_Head приводит к увеличению его размера в 2 раза.

В результате настройки сбрасывается флаг Py_TPFLAGS_HAVE_GC, базовый размер экземпляра tp_basicsize увеличивается на величину, необходимую для хранения дополнительных слотов для полей. Для достижения такого эффекта используется специальный метакласс datatype, который обеспечивает настройку подклассов dataobject. Метакласс datatype также обеспечивает установку значений слотов tp_alloc, tp_new, tp_dealloc, tp_free, которые реализуют корректные алгоритмы создания и разрушения экземпляров в памяти. Соответствующие имена полей перечисляются при объявлении класса (у класса Point их два: x и y). По умолчанию, у экземпляров отсутствуют __weakref__ и __dict__ (как и у экземпляров классов со __slots__).

Заключение

Это позволит уменьшить их след в памяти на величину заголовка PyGC_Head. Как можно было убедиться, в CPython при необходимости возможно отключение механизма циклической сборки мусора для конкретного класса, когда есть уверенность в том, что его экземпляры не будут образовывать циклические ссылки.

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»