Начиная с версии 3.12 Python поддерживает такой тип объектов, как бессмертные (Immortal). Бессмертными объектами являются глобальные константы, такие как None, False, True, а также некоторые другие объекты. Если вам интересно, что это за объекты, как ими становятся обычные смертные, где они используются и как повлияли на CPython — добро пожаловать.

Бессмертные объекты являются частью реализации CPython и не доступны пользователям Python или разработчикам C‑расширений для использования. Другие реализации Python могут использовать такой же подход как и CPython или создать собственную реализацию, базируясь на спецификации, описанной в PEP 683 — Immortal Objects, Using a Fixed Refcount.

Замечание относительно PEP
Стоит иметь в виду, то что описание, представленное в PEP, имеет исторический характер и может быть актуально только на дату его принятия. Со временем, под натиском опыта реального использования, реализация может претерпевать значительные изменения.

Причины появления

Идея добавления бессмертных объектов появилась в попытках реализовать эффективные механизмы многоядерного/многопоточного взаимодействия в Python. Проблема использования существующего модуля threading для таких задач известна давно и связана с наличием общего, для всех потоков, GIL. Существуют, также давно известные, решения — это выполнение расчетов в отдельных процессах и использование С‑расширений, которые отпускают GIL при выполнении расчетов (например, numpy использует Py_BEGIN_ALLOW_THREADS и Py_END_ALLOW_THREADS). Оба эти подхода имеют и негативные эффекты: использование множества процессов требует большего количества памяти и большего времени на запуск процессов. Использование пары Py_BEGIN_ALLOW_THREADS/Py_END_ALLOW_THREADS не позволяет выполнять какой‑либо питоновский код между BEGIN и END.

Эти проблемы привели к появлению двух новых решений: первое это полный отказ от GIL — PEP 703 — Making the Global Interpreter Lock Optional in CPython — или как все его называются free-threading. Второе решение — субинтерпретаторы — PEP 734 — Multiple Interpreters in the Stdlib — каждый субинтерпретатор может иметь свой собственный GIL, а субинтерпретатор запускается поверх отдельного потока (это один из режимов, который важен в контексте статьи).

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

Реализация любых объектов Python содержит служебную информацию. И эта информация постоянно претерпевает изменения, даже если эти объекты являются логически неизменяемыми (например, байты, строки, числа, кортежи, статические типы, глобальные объекты).

Всякий раз, когда мы используем какой‑то объект — передаем его в функцию, сохраняем на него ссылку в локальной переменной или в глобальном объекте — мы выполняем операции увеличения и уменьшения значения счетчика ссылок.
Если объект интенсивно используется из нескольких потоков, то чтобы избежать появления data-races потребуется использованием либо атомарных операций, либо блокировок. По этому пути пошел PEP-703 (можно посмотреть код обернутый в Py_GIL_DISABLED).

Другой подход выбрал PEP-734 — избавиться от конкурентного доступа к счетчику ссылок можно, если объект используется потоком/субинтерпретатором эксклюзивно, либо у объекта отсутствуют операции, которые изменяют его счетчик ссылок (что интересно, такой подход будет эффективен и в первом случае). Соответственно, объекты, у которых не меняется счетчик ссылок при их использовании, и стали одной из причин появления бессмертных объектов в CPython.

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

Все это относится к логически неизменяемым данным. К этим данным относятся интернированные строки (все бессмертные строки интернированы), константы, статические типы. Они разделяются всеми интерпретаторами, принадлежат главному и будут уничтожены в момент уничтожения/финализации рантайма.

К сожалению, положительного эффекта такая оптимизация не дала. Первоначальная реализация бессмертных объектов показывала порядка 2% деградации производительности. Но концептуально это решение было важно и поэтому PEP-683 был принят.

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

Аналогичная проблема возникла при создании субинтерпретаторов. Связь в том, что субинтерпретаторы должно довольно много состояния разделять между друг другом (между главным и дочерними). К этому состоянию относятся типы, встроенные модули, функции, те же интернированные объекты. И использование бессмертных объектов значительно упрощает реализацию интерпретаторов, связанную с их созданием и финализацией, а также повышает быстродействие создания субинтерпретаторов.

Эффекты

Как было сказано ранее, бессмертные объекты являются частью реализации и не доступны конечному пользователю (помимо того, что это влияет на пользователя, это влияет и на другие реализации Python). Но часть эффектов от их появления видна снаружи. Они описаны в PEP-683, часть из них безобидна, а часть может повлиять на вашу систему, я кратко их перескажу.

Проверка временем
Но, т.к. эта функциональность появилась в 3.12, а на дворе скоро 3.14 и бессмертные объекты широко используются внутри CPython — то реальных проблем с ними не известно.

Первый эффект связан с особенностью реализации бессмертных объектов в CPython. Для их обозначения используется очень большое число для значения счетчика ссылок, которое в реальности не должно быть достигнуто. Этот факт влияет на то, что значение, которое возвращает sys.getrefcount не может/должно интерпретироваться как реально значение счетчика ссылок. И имеет смысл (и это документировано) работать только с двумя значениями — 0 — объектом никто не владеет и он может быть уничтожен, 1 — вызывающий код является единственным владельцем объекта, и с ним тоже можно делать всё что угодно. В природе наверняка существует код, который в том или ином виде зависел от значения счетчика ссылок у объекта и в этом случае он может сломаться при работе с бессмертными объектами.

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

Следующий сценарий позволяет превратить объект в бессмертный и получить утечку памяти.

import sys
from test.support import _2G

c = object()
l = [c] * (_2G - 20)
for _ in range(30):
    l.append(c)

Запуск этого кода следующим образом python -X showrefcount .\immortalize.py приведет к следующему результату:

.\python.bat -X showrefcount .\immortalize.py
[2147483647 refs, 1 blocks]

Результат [2147483647 refs, 1 blocks] показывает, что имеется большое количество неосвобожденных объектов. Как видно сценарий довольно искусственный, т.к. даже небольшие изменения в нем (например, заменить создание объекта на что‑то более тяжеловесное) может привести к MemoryError и аварийному завершению.

Для появления третьего эффекта — Accidental De‑Immortalizing — необходимо наличие нескольких факторов — использование старого стабильного ABI (в этом случае операция увеличения и уменьшения счетчика ссылок реализованы как макросы и напрямую изменяют значение в объекте), использование 32-битной версии и опять особый сценарий. Допустим, мы неограниченно «увеличиваем» значение счетчика у бессмертных объектов и, начиная с определенного числа итераций, мы дойдем до специального значения счетчика ссылок и начнем теперь уменьшать его значение, так как Py_DECREF ничего не знает о бессмертных объектах — он просто начнет уменьшать счетчик. И в определенный момент дойдет до 0 и попытается удалить статически созданный объект, что приведет к аварийному завершению процесса.

У меня, к сожалению или к счастью, не получилось воспроизвести такой сценарий. Более подробное описание можно найти здесь.

Бессмертные объекты и сборка мусора

Сборка мусора это механизм автоматического управления временем жизни созданных объектов. В CPython используется механизм подсчета ссылок, который заключается в использовании функций Py_INCREF и Py_DECREF, когда объект используется и когда нет, соответственно. Когда значение счетчика ссылок доходит до 0, то это означает, что объектом больше никто не владеет и он может быть безопасно удален.

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

Объекты, которые поддерживают сборку мусора имеют, следующий memory layout:

                  +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ \
                  |                    *_gc_next                  | |
                  +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | PyGC_Head
                  |                    *_gc_prev                  | |
    object -----> +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ /
                  |                    ob_refcnt                  | \
                  +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | PyObject_HEAD
                  |                    *ob_type                   | |
                  +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ /
                  |                      ...                      |

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

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

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

Поэтому бессмертные объекты при иммортализации исключаются из сборщика мусора.

void
_Py_SetImmortal(PyObject *op)
{
    if (PyObject_IS_GC(op) && _PyObject_GC_IS_TRACKED(op)) {
        _PyObject_GC_UNTRACK(op);
    }
    _Py_SetImmortalUntracked(op);
}

Также существует дополнительная проверка, в самом сборщике мусора для исключения объектов такого типа.

static void
update_refs(PyGC_Head *containers)
{
    PyGC_Head *next;
    PyGC_Head *gc = GC_NEXT(containers);

    while (gc != containers) {
        next = GC_NEXT(gc);
        PyObject *op = FROM_GC(gc);
        if (_Py_IsImmortal(op)) {
			assert(!_Py_IsStaticImmortal(op));
			_PyObject_GC_UNTRACK(op);
			gc = next;
			continue;
        }
...
}

Бессмертные объекты и субинтерпретаторы

Интересный факт — если посмотреть на код реализации субинтерпретаторов, то никаких отсылок к бессмертным объектам найти не удастся. При этом они являются неотъемлемой основой субинтерпретаторов в CPython. Как это происходит?

Как уже упоминалось выше, к бессмертным объектам относятся:

  1. статически выделенные константы — None, False, True, ..., NotImplementedType, 0, 1, '', b'', (). (object.c:_Py_GetConstant_Init)

  2. целые числа в диапазоне [-5, 256] (pycore_runtime_structs.h:_Py_static_objects)

  3. символы в кодировке latin1

  4. статические типы (_Py_TPFLAGS_STATIC_BUILTIN), а также их компоненты, такие как, базовый класс, список базовых классов, MRO (method‑resolution order)

  5. интернированные строки и идентификаторы (pycore_runtime_structs.h:_Py_cached_objects)

Рассмотрим функциюnew_interpreter, которая используется для создания как главного интерпретатора, так и субинтерпретаторов:

static PyStatus
new_interpreter(PyThreadState **tstate_p,
                const PyInterpreterConfig *config, long whence)
{
	...

    PyInterpreterState *interp = PyInterpreterState_New();
    if (interp == NULL) {
        *tstate_p = NULL;
        return _PyStatus_OK();
    }

	...

    /* No objects have been created yet. */

    status = pycore_interp_init(tstate);
    if (_PyStatus_EXCEPTION(status)) {
        goto error;
    }

    ...
    return _PyStatus_OK();
}

В этой функции нас будут интересовать два вызова — PyInterpreterState_New и pycore_interp_init. Вызов PyInterpreterState_New, приводит по цепочке к init_interpreter, где можно увидеть следующий код:

static PyStatus
init_interpreter(PyInterpreterState *interp,
                 _PyRuntimeState *runtime, int64_t id,
                 PyInterpreterState *next,
                 long whence)
{
	...

    assert(runtime != NULL);
    interp->runtime = runtime;

	...
    return _PyStatus_OK();
}

т. к. между интерпретаторами просто копируется указатель на рантайм, то это значит, что каждому интерпретатору доступны статические и кешированные объекты (_Py_static_objects и _Py_cached_objects, соответственно) без необходимости хранить копию каждого из объектов.

А pycore_interp_init более объемная функция:

static PyStatus
pycore_interp_init(PyThreadState *tstate)
{
    ...

    // Create singletons before the first PyType_Ready() call, since
    // PyType_Ready() uses singletons like the Unicode empty string (tp_doc)
    // and the empty tuple singletons (tp_bases).
    status = pycore_init_global_objects(interp);
    if (_PyStatus_EXCEPTION(status)) {
        return status;
    }

    ...

    status = pycore_init_types(interp);
    if (_PyStatus_EXCEPTION(status)) {
        goto done;
    }

    ...

    status = pycore_init_builtins(tstate);
    if (_PyStatus_EXCEPTION(status)) {
        goto done;
    }

    ...

done:
    /* sys.modules['sys'] contains a strong reference to the module */
    Py_XDECREF(sysmod);
    return status;
}

static PyStatus
pycore_init_global_objects(PyInterpreterState *interp)
{
    PyStatus status;

    _PyFloat_InitState(interp);

    status = _PyUnicode_InitGlobalObjects(interp);
    if (_PyStatus_EXCEPTION(status)) {
        return status;
    }

    _PyUnicode_InitState(interp);

    if (_Py_IsMainInterpreter(interp)) {
        _Py_GetConstant_Init();
    }

    return _PyStatus_OK();
}

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

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

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

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

Сериализация передаваемых данных может быть настроена довольно гибко с использованием структуры _PyXIData_t (pycore_crossinterp.h) (можно даже сказать, что это реализация полиморфизма вручную). И для таких объектов как None, True, False, кортеж, строка и целые числа десериализация данных выполняется без создания новых объектов, если десериализуемые объекты являются статическими константами. Например, для None или bool это выглядит следующим образом:

// None

static PyObject *
_new_none_object(_PyXIData_t *xidata)
{
    // XXX Singleton refcounts are problematic across interpreters...
    return Py_NewRef(Py_None);
}

static int
_none_shared(PyThreadState *tstate, PyObject *obj, _PyXIData_t *xidata)
{
    _PyXIData_Init(xidata, tstate->interp, NULL, NULL, _new_none_object);
    // xidata->data, xidata->obj and xidata->free remain NULL
    return 0;
}

// bool

static PyObject *
_new_bool_object(_PyXIData_t *xidata)
{
    if (xidata->data){
        Py_RETURN_TRUE;
    }
    Py_RETURN_FALSE;
}

static int
_bool_shared(PyThreadState *tstate, PyObject *obj, _PyXIData_t *xidata)
{
    _PyXIData_Init(xidata, tstate->interp,
            (void *) (Py_IsTrue(obj) ? (uintptr_t) 1 : (uintptr_t) 0), NULL,
            _new_bool_object);
    // xidata->obj and xidata->free remain NULL
    return 0;
}

Как видно, _new_none_object и _new_bool_objectне создают новых объектов (и на самом деле могут не изменять значение счетчика ссылок). Для других бессмертных неизменяемых объектов операции реализованы аналогично.

Детали реализации

Функциями, которые отвечают за иммортализацию, являются _Py_SetImmortal и _Py_SetImmortalUntracked, за проверку является ли объект бессмертным — _Py_IsImmortal и _Py_IsStaticImmortal, вернуть объект в мир смертных _Py_SetMortal (хотя Марк и считает это нонсенсом).

На данный момент, существует несколько версий работы с бессмертными объектами, мы рассмотрим только базовые версии для 64-битной и 32-битной сборки Python. Очищенный код для интересующих функций ниже:

#if SIZEOF_VOID_P > 4
// 64 bit
_Py_IMMORTAL_INITIAL_REFCNT = 3 << 30
// 3_221_225_472
// 1100 0000 0000 0000 0000 0000 0000 0000

_Py_IMMORTAL_MINIMUM_REFCNT = 1 << 31 // (2 << 30 == 2**31)
// 2_147_483_648
// 1000 0000 0000 0000 0000 0000 0000 0000

#else
// 32 bit
_Py_IMMORTAL_INITIAL_REFCNT = 5 << 28
// 1_342_177_280
// 0101 0000 0000 0000 0000 0000 0000 0000

_Py_IMMORTAL_MINIMUM_REFCNT = 1 << 30 // (4 << 28)
// 1_073_741_824
// 0100 0000 0000 0000 0000 0000 0000 0000
#endif

#define _Py_IMMORTAL_FLAGS (1 << 0)

void
_Py_SetImmortalUntracked(PyObject *op)
{
    // Check if already immortal to avoid degrading from static immortal to plain immortal
    if (_Py_IsImmortal(op)) {
        return;
    }
#if SIZEOF_VOID_P > 4
    op->ob_flags = _Py_IMMORTAL_FLAGS;
    op->ob_refcnt = _Py_IMMORTAL_INITIAL_REFCNT;
#else
    op->ob_refcnt = _Py_IMMORTAL_INITIAL_REFCNT;
#endif
}

void
_Py_SetImmortal(PyObject *op)
{
    if (PyObject_IS_GC(op) && _PyObject_GC_IS_TRACKED(op)) {
        _PyObject_GC_UNTRACK(op);
    }
    _Py_SetImmortalUntracked(op);
}

static inline Py_ALWAYS_INLINE int _Py_IsImmortal(PyObject *op)
{
#if SIZEOF_VOID_P > 4
    return _Py_CAST(PY_INT32_T, op->ob_refcnt) < 0;
#else
    return op->ob_refcnt >= _Py_IMMORTAL_MINIMUM_REFCNT;
#endif
}

static inline Py_ALWAYS_INLINE void Py_INCREF(PyObject *op)
{
#if SIZEOF_VOID_P > 4
    PY_UINT32_T cur_refcnt = op->ob_refcnt;
    if (cur_refcnt >= _Py_IMMORTAL_INITIAL_REFCNT) {
        return;
    }
    op->ob_refcnt = cur_refcnt + 1;
#else
    if (_Py_IsImmortal(op)) {
        return;
    }
    op->ob_refcnt++;
#endif
}

static inline Py_ALWAYS_INLINE void Py_DECREF(PyObject *op)
{
    if (_Py_IsImmortal(op)) {
        return;
    }
    if (--op->ob_refcnt == 0) {
        _Py_Dealloc(op);
    }
}

Итак, что здесь интересного — для работы со значением счетчика ссылок используется две числовые константы _Py_IMMORTAL_INITIAL_REFCNT и _Py_IMMORTAL_MINIMUM_REFCNT. Их значения подобраны таким образом, чтобы минимизировать вероятность появления проблем, связанных с Accidental De‑Immortalizing.

Если объект намеренно устанавливается бессмертным, то для счетчика ссылок используется значение _Py_IMMORTAL_INITIAL_REFCNT, которое также является максимальным значением для счетчика ссылок. Далее есть различия для 64 и 32-битной версий. Рассмотрим сначала 64-битную версию.

В 64-битной версии значение _Py_IMMORTAL_MINIMUM_REFCNT является максимальным значением для int32_t, а значит любое значение счетчика ссылок между _Py_IMMORTAL_MINIMUM_REFCNT и _Py_IMMORTAL_INITIAL_REFCNT, если его скастовать в int32_t является отрицательным и этот факт используется при проверке является ли объект бессмертным. Также, насколько я могу судить по godbolt, эта версия является более эффективной.

Также в этом диапазоне разрешено увеличивать значение счетчика ссылок, для того, чтобы «поправить» это значение для бессмертных объектов, у которых оно было случайно уменьшено. Но уменьшать это значение разрешено, только если оно меньше _Py_IMMORTAL_MINIMUM_REFCNT — т. е. если значение ниже этой границы, то объект перестает считаться бессмертным. Это актуально в случае если какое‑либо С‑расширение использует версию стабильного ABI ниже 3.12 (это проявление Accidental De‑Immortalizing).

Такая проблема может возникнуть следующим образом — например, приложение Python использует С‑расширения с более старой версией ABI. Это означает, что C‑расширение использует версии макросов Py_INCREF и Py_DECREF, в которых отсутствуют проверки на иммортализацию объектов. Если это С‑расширение будет вызывать Py_DECREF для уже бессмертного объекта, то оно должно будет совершить более 1 миллиарда таких вызовов (т. е. снизить значение счетчика ссылок от начального _Py_IMMORTAL_INITIAL_REFCNT до граничного _Py_IMMORTAL_MINIMUM_REFCNT), чтобы объект перестал быть бессмертным. Предполагается, что такая ситуация маловероятна.

В случае 32-битной версии рассуждения аналогичные. Только значения констант, в связи с тем, что свободных значений гораздо меньше, подобраны несколько иначе. И случайная де‑иммортализация произойдет раньше — через 300 тысяч вызовов.

Заключение

Как было показано выше, бессмертные объекты обладают как полезными свойствами, так и негативными эффектами (которые на самом деле на практике довольно тяжело встретить).

Прироста производительности они не дали и показывали даже некоторое замедление (https://github.com/python/cpython/pull/19474), но имеющиеся положительные эффекты от их внедрения перевесили незначительное проседание производительности.

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

И одно из направлений — дать пользователям возможность указывать, что их объекты, тоже являются бессмертными. В этом направлении готовится новый PEP, который уже получил номер — PEP 797: Arbitrary Object Immortalization, а значит, и поддержку среди разработчиков языка.

Если вас заинтересовала тема субинтерпретаторов, то можно ознакомиться с публикациями коллег по опасному бизнесу:

  1. PEP-734: Субинтерпретаторы в Python 3.14

  2. Запускаем несколько интерпретаторов в коде на Python — невероятная скорость

Спасибо за внимание.

Комментарии (4)


  1. anonymous
    20.07.2025 09:32


    1. zzzzzzerg Автор
      20.07.2025 09:32

      можно спокойно забыть про проблемы гонки за счётчиками ссылок.

      Увы, но только у бессмертных объектов, которых по факту не так уж и много.


  1. DrArgentum
    20.07.2025 09:32

    Отличная статья, коллега!


  1. Eclips4
    20.07.2025 09:32

    Спасибо за статью, Сергей!