Начиная с версии 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. Как это происходит?
Как уже упоминалось выше, к бессмертным объектам относятся:
статически выделенные константы —
None
,False
,True
,...
,NotImplementedType
,0
,1
,''
,b''
,()
. (object.c:_Py_GetConstant_Init
)целые числа в диапазоне [-5, 256] (
pycore_runtime_structs.h:_Py_static_objects
)символы в кодировке latin1
статические типы (
_Py_TPFLAGS_STATIC_BUILTIN
), а также их компоненты, такие как, базовый класс, список базовых классов,MRO
(method‑resolution order)интернированные строки и идентификаторы (
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, а значит, и поддержку среди разработчиков языка.
Если вас заинтересовала тема субинтерпретаторов, то можно ознакомиться с публикациями коллег по опасному бизнесу:
Спасибо за внимание.
anonymous
zzzzzzerg Автор
Увы, но только у бессмертных объектов, которых по факту не так уж и много.