PythonMiddleTechnical

Что такое reference counting?

Reference counting — механизм управления памятью в CPython: у каждого PyObject поле ob_refcnt. Каждое создание ссылки делает Py_INCREF, каждое удаление — Py_DECREF. При ob_refcnt == 0 объект освобождается немедленно (вызывается __del__, потом tp_dealloc). Не справляется с циклами — для них есть cyclic GC.

Идея и устройство

CPython управляет временем жизни объектов через подсчёт ссылок. Каждый PyObject имеет поле ob_refcnt (целое число). Когда создаётся новая ссылка (присваивание, добавление в список, передача в функцию) — счётчик растёт. Когда ссылка исчезает (выход из scope, del, перепривязка имени) — падает. Когда падает до 0, объект освобождается немедленно.

Эта модель отличается от трасинг-GC (Java, Go): не нужны паузы для маркировки, объекты освобождаются предсказуемо и сразу.

Что увеличивает refcount

import sys

obj = {"name": "cache"}        # refcount = 1
print(sys.getrefcount(obj))    # 2 (наша ссылка + временная в getrefcount)

# +1 при добавлении в контейнер
refs = [obj]
print(sys.getrefcount(obj))    # 3

# +1 при передаче в функцию (на время вызова)
def take(x):
    print(sys.getrefcount(x))  # 4 — наша, контейнер, x в функции, временная

take(obj)

# del уменьшает только эту ссылку
del refs
print(sys.getrefcount(obj))    # 2

Что del делает и не делает

del name — удаляет имя name из текущего scope. Это уменьшает refcount объекта на 1; если других ссылок нет — объект освобождается, иначе живёт дальше.

a = [1, 2, 3]
b = a               # refcount = 2
del a               # del уменьшил до 1, объект жив через b
print(b)            # [1, 2, 3]

Когда вызывается __del__

При освобождении объекта Python вызывает __del__ (если есть), потом tp_dealloc, потом освобождает память. Важно: __del__ ≠ deconstructor C++. Он не гарантирует мгновенный вызов в случаях циклов или прерванного интерпретатора.

Проблема циклов

class Node:
    pass

a = Node(); b = Node()
a.partner = b
b.partner = a

print(sys.getrefcount(a))   # 3
del a, b                    # внешних имён нет — но в куче по ссылке друг на друга

# refcount у обоих остался = 1
# Освободит их только cyclic GC: gc.collect()

Cyclic GC (mark-and-sweep по поколениям) — отдельный механизм для container-объектов, см. py-gc-needed-065.

Производительность и стоимость

  • Очень частые Py_INCREF/Py_DECREF — основной overhead CPython, ~10% времени выполнения.
  • Branch на --ob_refcnt == 0 в каждом DECREF.
  • Atomics для thread-safe-refcount при отсутствии GIL (PEP 703 — free-threaded CPython 3.13) дороже обычных операций.

Малоизвестные нюансы

  • Immortal objects (PEP 683, 3.12+): синглтоны вроде None, True, False, маленькие int имеют specially-large refcount и не освобождаются никогда.
  • sys.getrefcount(x) возвращает значение, которое включает временную ссылку от самого вызова — учитывайте.
  • При завершении интерпретатора некоторые объекты могут не пройти __del__ — порядок неопределён.

Подводные камни

  • Полагаться на __del__ для освобождения ресурсов — для циклов сработает поздно, для shutdown может вообще не сработать. Используйте with и контекст-менеджеры.
  • Считать sys.getrefcount(x) точным production-инструментом — добавляет временную ссылку, и в многопоточном коде неустойчив.
  • Не понимать, что del удаляет имя, а не объект.
  • Большие циклические структуры (двусвязный список) — без cyclic GC будут расти; в hot path можно делать weakref.
  • Free-threaded CPython 3.13 меняет схему: biased reference counting, и поведение sys.getrefcount под нагрузкой иное.
  • Утечка в C-extension через Py_INCREF без Py_DECREF приводит к росту памяти, который не виден из gc-модуля.

Common mistakes

  • Путать имя и объект.
  • Не упоминать CPython.
  • Не связывать refcount с GIL и памятью.

What the interviewer is testing

  • Объясняет увеличение/уменьшение refcount.
  • Знает ограничение циклов.
  • Понимает del как удаление ссылки.

Sources

Related topics