Skip to content

Commit 58de753

Browse files
authored
[mypyc] Reduce impact of immortality on reference counting performance (#18459)
Fixes mypyc/mypyc#1044. The addition of object immortality in Python 3.12 (PEP 683) introduced an extra immortality check to incref and decref operations. Objects with a specific reference count are treated as immortal, and their reference counts are never updated. It turns out that this slowed down the performance of certain workloads a lot (up to 70% increase in runtime, compared to 3.11). This PR reduces the impact of immortality via a few optimizations: 1. Assume instances of native classes and list objects are not immortal (skip immortality checks). 2. Skip incref of certain objects in some contexts when we know that they are immortal (e.g. avoid incref of `None`). The second change should be clear. We generally depend on CPython implementation details to improve performance, and this seems safe to do here as well. The first change could turn immortal objects into non-immortal ones. For native classes this is a decision we can arguably make -- native classes don't properly support immortality, and they can't be shared between subinterpreters. As discussed in PEP 683, skipping immortality checks here is acceptable even in cases where somebody tries to make a native instance immortal, but this could have some performance or memory use impact. The performance gains make this a good tradeoff. Since lists are mutable, they can't be safely shared between subinterpreters, so again not dealing with immortality is acceptable. It could reduce performance in some use cases by deimmortalizing lists, but this potential impact seems marginal compared to faster incref and decref operations on lists, which are some of the more common objects in Python programs. This speeds up self check by about 1.5% on Python 3.13. This speeds up the richards benchmark by 30-35% (!) on 3.13, and also some other benchmarks see smaller improvements.
1 parent 43ea203 commit 58de753

File tree

7 files changed

+241
-18
lines changed

7 files changed

+241
-18
lines changed

mypyc/codegen/emit.py

+10-3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
ATTR_PREFIX,
1313
BITMAP_BITS,
1414
FAST_ISINSTANCE_MAX_SUBCLASSES,
15+
HAVE_IMMORTAL,
1516
NATIVE_PREFIX,
1617
REG_PREFIX,
1718
STATIC_PREFIX,
@@ -511,8 +512,11 @@ def emit_inc_ref(self, dest: str, rtype: RType, *, rare: bool = False) -> None:
511512
for i, item_type in enumerate(rtype.types):
512513
self.emit_inc_ref(f"{dest}.f{i}", item_type)
513514
elif not rtype.is_unboxed:
514-
# Always inline, since this is a simple op
515-
self.emit_line("CPy_INCREF(%s);" % dest)
515+
# Always inline, since this is a simple but very hot op
516+
if rtype.may_be_immortal or not HAVE_IMMORTAL:
517+
self.emit_line("CPy_INCREF(%s);" % dest)
518+
else:
519+
self.emit_line("CPy_INCREF_NO_IMM(%s);" % dest)
516520
# Otherwise assume it's an unboxed, pointerless value and do nothing.
517521

518522
def emit_dec_ref(
@@ -540,7 +544,10 @@ def emit_dec_ref(
540544
self.emit_line(f"CPy_{x}DecRef({dest});")
541545
else:
542546
# Inlined
543-
self.emit_line(f"CPy_{x}DECREF({dest});")
547+
if rtype.may_be_immortal or not HAVE_IMMORTAL:
548+
self.emit_line(f"CPy_{x}DECREF({dest});")
549+
else:
550+
self.emit_line(f"CPy_{x}DECREF_NO_IMM({dest});")
544551
# Otherwise assume it's an unboxed, pointerless value and do nothing.
545552

546553
def pretty_name(self, typ: RType) -> str:

mypyc/codegen/emitfunc.py

+18
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from mypyc.analysis.blockfreq import frequently_executed_blocks
88
from mypyc.codegen.emit import DEBUG_ERRORS, Emitter, TracebackAndGotoHandler, c_array_initializer
99
from mypyc.common import (
10+
HAVE_IMMORTAL,
1011
MODULE_PREFIX,
1112
NATIVE_PREFIX,
1213
REG_PREFIX,
@@ -76,9 +77,11 @@
7677
RStruct,
7778
RTuple,
7879
RType,
80+
is_bool_rprimitive,
7981
is_int32_rprimitive,
8082
is_int64_rprimitive,
8183
is_int_rprimitive,
84+
is_none_rprimitive,
8285
is_pointer_rprimitive,
8386
is_tagged,
8487
)
@@ -578,6 +581,21 @@ def emit_method_call(self, dest: str, op_obj: Value, name: str, op_args: list[Va
578581
)
579582

580583
def visit_inc_ref(self, op: IncRef) -> None:
584+
if (
585+
isinstance(op.src, Box)
586+
and (is_none_rprimitive(op.src.src.type) or is_bool_rprimitive(op.src.src.type))
587+
and HAVE_IMMORTAL
588+
):
589+
# On Python 3.12+, None/True/False are immortal, and we can skip inc ref
590+
return
591+
592+
if isinstance(op.src, LoadLiteral) and HAVE_IMMORTAL:
593+
value = op.src.value
594+
# We can skip inc ref for immortal literals on Python 3.12+
595+
if type(value) is int and -5 <= value <= 256:
596+
# Small integers are immortal
597+
return
598+
581599
src = self.reg(op.src)
582600
self.emit_inc_ref(src, op.src.type)
583601

mypyc/common.py

+6
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,12 @@
8282
"pythonsupport.c",
8383
]
8484

85+
# Python 3.12 introduced immortal objects, specified via a special reference count
86+
# value. The reference counts of immortal objects are normally not modified, but it's
87+
# not strictly wrong to modify them. See PEP 683 for more information, but note that
88+
# some details in the PEP are out of date.
89+
HAVE_IMMORTAL: Final = sys.version_info >= (3, 12)
90+
8591

8692
JsonDict = dict[str, Any]
8793

mypyc/ir/rtypes.py

+42-3
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from typing import TYPE_CHECKING, ClassVar, Final, Generic, TypeVar
2727
from typing_extensions import TypeGuard
2828

29-
from mypyc.common import IS_32_BIT_PLATFORM, PLATFORM_SIZE, JsonDict, short_name
29+
from mypyc.common import HAVE_IMMORTAL, IS_32_BIT_PLATFORM, PLATFORM_SIZE, JsonDict, short_name
3030
from mypyc.namegen import NameGenerator
3131

3232
if TYPE_CHECKING:
@@ -69,6 +69,11 @@ def accept(self, visitor: RTypeVisitor[T]) -> T:
6969
def short_name(self) -> str:
7070
return short_name(self.name)
7171

72+
@property
73+
@abstractmethod
74+
def may_be_immortal(self) -> bool:
75+
raise NotImplementedError
76+
7277
def __str__(self) -> str:
7378
return short_name(self.name)
7479

@@ -151,6 +156,10 @@ class RVoid(RType):
151156
def accept(self, visitor: RTypeVisitor[T]) -> T:
152157
return visitor.visit_rvoid(self)
153158

159+
@property
160+
def may_be_immortal(self) -> bool:
161+
return False
162+
154163
def serialize(self) -> str:
155164
return "void"
156165

@@ -193,6 +202,7 @@ def __init__(
193202
ctype: str = "PyObject *",
194203
size: int = PLATFORM_SIZE,
195204
error_overlap: bool = False,
205+
may_be_immortal: bool = True,
196206
) -> None:
197207
RPrimitive.primitive_map[name] = self
198208

@@ -204,6 +214,7 @@ def __init__(
204214
self._ctype = ctype
205215
self.size = size
206216
self.error_overlap = error_overlap
217+
self._may_be_immortal = may_be_immortal and HAVE_IMMORTAL
207218
if ctype == "CPyTagged":
208219
self.c_undefined = "CPY_INT_TAG"
209220
elif ctype in ("int16_t", "int32_t", "int64_t"):
@@ -230,6 +241,10 @@ def __init__(
230241
def accept(self, visitor: RTypeVisitor[T]) -> T:
231242
return visitor.visit_rprimitive(self)
232243

244+
@property
245+
def may_be_immortal(self) -> bool:
246+
return self._may_be_immortal
247+
233248
def serialize(self) -> str:
234249
return self.name
235250

@@ -433,8 +448,12 @@ def __hash__(self) -> int:
433448
"builtins.None", is_unboxed=True, is_refcounted=False, ctype="char", size=1
434449
)
435450

436-
# Python list object (or an instance of a subclass of list).
437-
list_rprimitive: Final = RPrimitive("builtins.list", is_unboxed=False, is_refcounted=True)
451+
# Python list object (or an instance of a subclass of list). These could be
452+
# immortal, but since this is expected to be very rare, and the immortality checks
453+
# can be pretty expensive for lists, we treat lists as non-immortal.
454+
list_rprimitive: Final = RPrimitive(
455+
"builtins.list", is_unboxed=False, is_refcounted=True, may_be_immortal=False
456+
)
438457

439458
# Python dict object (or an instance of a subclass of dict).
440459
dict_rprimitive: Final = RPrimitive("builtins.dict", is_unboxed=False, is_refcounted=True)
@@ -642,6 +661,10 @@ def __init__(self, types: list[RType]) -> None:
642661
def accept(self, visitor: RTypeVisitor[T]) -> T:
643662
return visitor.visit_rtuple(self)
644663

664+
@property
665+
def may_be_immortal(self) -> bool:
666+
return False
667+
645668
def __str__(self) -> str:
646669
return "tuple[%s]" % ", ".join(str(typ) for typ in self.types)
647670

@@ -763,6 +786,10 @@ def __init__(self, name: str, names: list[str], types: list[RType]) -> None:
763786
def accept(self, visitor: RTypeVisitor[T]) -> T:
764787
return visitor.visit_rstruct(self)
765788

789+
@property
790+
def may_be_immortal(self) -> bool:
791+
return False
792+
766793
def __str__(self) -> str:
767794
# if not tuple(unnamed structs)
768795
return "{}{{{}}}".format(
@@ -823,6 +850,10 @@ def __init__(self, class_ir: ClassIR) -> None:
823850
def accept(self, visitor: RTypeVisitor[T]) -> T:
824851
return visitor.visit_rinstance(self)
825852

853+
@property
854+
def may_be_immortal(self) -> bool:
855+
return False
856+
826857
def struct_name(self, names: NameGenerator) -> str:
827858
return self.class_ir.struct_name(names)
828859

@@ -883,6 +914,10 @@ def make_simplified_union(items: list[RType]) -> RType:
883914
def accept(self, visitor: RTypeVisitor[T]) -> T:
884915
return visitor.visit_runion(self)
885916

917+
@property
918+
def may_be_immortal(self) -> bool:
919+
return any(item.may_be_immortal for item in self.items)
920+
886921
def __repr__(self) -> str:
887922
return "<RUnion %s>" % ", ".join(str(item) for item in self.items)
888923

@@ -953,6 +988,10 @@ def __init__(self, item_type: RType, length: int) -> None:
953988
def accept(self, visitor: RTypeVisitor[T]) -> T:
954989
return visitor.visit_rarray(self)
955990

991+
@property
992+
def may_be_immortal(self) -> bool:
993+
return False
994+
956995
def __str__(self) -> str:
957996
return f"{self.item_type}[{self.length}]"
958997

mypyc/lib-rt/mypyc_util.h

+29
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,35 @@
3131
// Here just for consistency
3232
#define CPy_XDECREF(p) Py_XDECREF(p)
3333

34+
// The *_NO_IMM operations below perform refcount manipulation for
35+
// non-immortal objects (Python 3.12 and later).
36+
//
37+
// Py_INCREF and other CPython operations check for immortality. This
38+
// can be expensive when we know that an object cannot be immortal.
39+
40+
static inline void CPy_INCREF_NO_IMM(PyObject *op)
41+
{
42+
op->ob_refcnt++;
43+
}
44+
45+
static inline void CPy_DECREF_NO_IMM(PyObject *op)
46+
{
47+
if (--op->ob_refcnt == 0) {
48+
_Py_Dealloc(op);
49+
}
50+
}
51+
52+
static inline void CPy_XDECREF_NO_IMM(PyObject *op)
53+
{
54+
if (op != NULL && --op->ob_refcnt == 0) {
55+
_Py_Dealloc(op);
56+
}
57+
}
58+
59+
#define CPy_INCREF_NO_IMM(op) CPy_INCREF_NO_IMM((PyObject *)(op))
60+
#define CPy_DECREF_NO_IMM(op) CPy_DECREF_NO_IMM((PyObject *)(op))
61+
#define CPy_XDECREF_NO_IMM(op) CPy_XDECREF_NO_IMM((PyObject *)(op))
62+
3463
// Tagged integer -- our representation of Python 'int' objects.
3564
// Small enough integers are represented as unboxed integers (shifted
3665
// left by 1); larger integers (larger than 63 bits on a 64-bit

0 commit comments

Comments
 (0)