Skip to content

Commit 5750690

Browse files
authored
[mypyc] Make type objects immortal if using free threading (#19538)
If they are not immortal, concurrent construction of objects by multiple threads can cause serious contention due to reference count updates. Making them immortal is similar to how both user-defined normal Python classes and built-in types in free-threaded builds are immortal. Fix the issue for both native and non-native classes. Dataclasses still have contention, and they may be harder to fix (this may require a fix in CPython). This speeds up a few micro-benchmarks that construct instances of classes in multiple threads by a big factor (5x+).
1 parent 0f78f9c commit 5750690

File tree

7 files changed

+56
-2
lines changed

7 files changed

+56
-2
lines changed

mypyc/irbuild/builder.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,10 @@ def new_tuple(self, items: list[Value], line: int) -> Value:
424424
def debug_print(self, toprint: str | Value) -> None:
425425
return self.builder.debug_print(toprint)
426426

427+
def set_immortal_if_free_threaded(self, v: Value, line: int) -> None:
428+
"""Make an object immortal on free-threaded builds (to avoid contention)."""
429+
self.builder.set_immortal_if_free_threaded(v, line)
430+
427431
# Helpers for IR building
428432

429433
def add_to_non_ext_dict(
@@ -433,6 +437,10 @@ def add_to_non_ext_dict(
433437
key_unicode = self.load_str(key)
434438
self.primitive_op(dict_set_item_op, [non_ext.dict, key_unicode, val], line)
435439

440+
# It's important that accessing class dictionary items from multiple threads
441+
# doesn't cause contention.
442+
self.builder.set_immortal_if_free_threaded(val, line)
443+
436444
def gen_import(self, id: str, line: int) -> None:
437445
self.imports[id] = None
438446

mypyc/irbuild/classdef.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,9 @@ def finalize(self, ir: ClassIR) -> None:
262262
non_ext_class = load_non_ext_class(self.builder, ir, self.non_ext, self.cdef.line)
263263
non_ext_class = load_decorated_class(self.builder, self.cdef, non_ext_class)
264264

265+
# Try to avoid contention when using free threading.
266+
self.builder.set_immortal_if_free_threaded(non_ext_class, self.cdef.line)
267+
265268
# Save the decorated class
266269
self.builder.add(
267270
InitStatic(non_ext_class, self.cdef.name, self.builder.module_name, NAMESPACE_TYPE)
@@ -449,6 +452,11 @@ def allocate_class(builder: IRBuilder, cdef: ClassDef) -> Value:
449452
)
450453
# Create the class
451454
tp = builder.call_c(pytype_from_template_op, [template, tp_bases, modname], cdef.line)
455+
456+
# Set type object to be immortal if free threaded, as otherwise reference count contention
457+
# can cause a big performance hit.
458+
builder.set_immortal_if_free_threaded(tp, cdef.line)
459+
452460
# Immediately fix up the trait vtables, before doing anything with the class.
453461
ir = builder.mapper.type_to_ir[cdef.info]
454462
if not ir.is_trait and not ir.builtin_base:

mypyc/irbuild/ll_builder.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from __future__ import annotations
88

9+
import sys
910
from collections.abc import Sequence
1011
from typing import Callable, Final, Optional
1112

@@ -16,6 +17,7 @@
1617
from mypyc.common import (
1718
BITMAP_BITS,
1819
FAST_ISINSTANCE_MAX_SUBCLASSES,
20+
IS_FREE_THREADED,
1921
MAX_LITERAL_SHORT_INT,
2022
MAX_SHORT_INT,
2123
MIN_LITERAL_SHORT_INT,
@@ -164,6 +166,7 @@
164166
fast_isinstance_op,
165167
none_object_op,
166168
not_implemented_op,
169+
set_immortal_op,
167170
var_object_size,
168171
)
169172
from mypyc.primitives.registry import (
@@ -2322,6 +2325,11 @@ def new_tuple_with_length(self, length: Value, line: int) -> Value:
23222325
def int_to_float(self, n: Value, line: int) -> Value:
23232326
return self.primitive_op(int_to_float_op, [n], line)
23242327

2328+
def set_immortal_if_free_threaded(self, v: Value, line: int) -> None:
2329+
"""Make an object immortal on free-threaded builds (to avoid contention)."""
2330+
if IS_FREE_THREADED and sys.version_info >= (3, 14):
2331+
self.primitive_op(set_immortal_op, [v], line)
2332+
23252333
# Internal helpers
23262334

23272335
def decompose_union_helper(

mypyc/lib-rt/CPy.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -931,6 +931,10 @@ PyObject *CPy_GetANext(PyObject *aiter);
931931
void CPy_SetTypeAliasTypeComputeFunction(PyObject *alias, PyObject *compute_value);
932932
void CPyTrace_LogEvent(const char *___location, const char *line, const char *op, const char *details);
933933

934+
#if CPY_3_14_FEATURES
935+
void CPy_SetImmortal(PyObject *obj);
936+
#endif
937+
934938
#ifdef __cplusplus
935939
}
936940
#endif

mypyc/lib-rt/misc_ops.c

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1058,7 +1058,7 @@ void CPyTrace_LogEvent(const char *___location, const char *line, const char *op, c
10581058

10591059
#endif
10601060

1061-
#ifdef CPY_3_12_FEATURES
1061+
#if CPY_3_12_FEATURES
10621062

10631063
// Copied from Python 3.12.3, since this struct is internal to CPython. It defines
10641064
// the structure of typing.TypeAliasType objects. We need it since compute_value is
@@ -1088,3 +1088,13 @@ void CPy_SetTypeAliasTypeComputeFunction(PyObject *alias, PyObject *compute_valu
10881088
}
10891089

10901090
#endif
1091+
1092+
#if CPY_3_14_FEATURES
1093+
1094+
#include "internal/pycore_object.h"
1095+
1096+
void CPy_SetImmortal(PyObject *obj) {
1097+
_Py_SetImmortal(obj);
1098+
}
1099+
1100+
#endif

mypyc/lib-rt/mypyc_util.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,9 @@ static inline CPyTagged CPyTagged_ShortFromSsize_t(Py_ssize_t x) {
139139
return x << 1;
140140
}
141141

142-
// Are we targeting Python 3.12 or newer?
142+
// Are we targeting Python 3.X or newer?
143143
#define CPY_3_12_FEATURES (PY_VERSION_HEX >= 0x030c0000)
144+
#define CPY_3_14_FEATURES (PY_VERSION_HEX >= 0x030e0000)
144145

145146
#if CPY_3_12_FEATURES
146147

mypyc/primitives/misc_ops.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,3 +311,18 @@
311311
return_type=void_rtype,
312312
error_kind=ERR_NEVER,
313313
)
314+
315+
# Mark object as immortal -- it won't be freed via reference counting, as
316+
# the reference count won't be updated any longer. Immortal objects support
317+
# fast concurrent read-only access from multiple threads when using free
318+
# threading, since this eliminates contention from concurrent reference count
319+
# updates.
320+
#
321+
# Needs at least Python 3.14.
322+
set_immortal_op = custom_primitive_op(
323+
name="set_immmortal",
324+
c_function_name="CPy_SetImmortal",
325+
arg_types=[object_rprimitive],
326+
return_type=void_rtype,
327+
error_kind=ERR_NEVER,
328+
)

0 commit comments

Comments
 (0)