diff --git a/mypyc/codegen/emit.py b/mypyc/codegen/emit.py index f6663e6194dc..bef560b3d42a 100644 --- a/mypyc/codegen/emit.py +++ b/mypyc/codegen/emit.py @@ -12,6 +12,7 @@ ATTR_PREFIX, BITMAP_BITS, FAST_ISINSTANCE_MAX_SUBCLASSES, + HAVE_IMMORTAL, NATIVE_PREFIX, REG_PREFIX, STATIC_PREFIX, @@ -511,8 +512,11 @@ def emit_inc_ref(self, dest: str, rtype: RType, *, rare: bool = False) -> None: for i, item_type in enumerate(rtype.types): self.emit_inc_ref(f"{dest}.f{i}", item_type) elif not rtype.is_unboxed: - # Always inline, since this is a simple op - self.emit_line("CPy_INCREF(%s);" % dest) + # Always inline, since this is a simple but very hot op + if rtype.may_be_immortal or not HAVE_IMMORTAL: + self.emit_line("CPy_INCREF(%s);" % dest) + else: + self.emit_line("CPy_INCREF_NO_IMM(%s);" % dest) # Otherwise assume it's an unboxed, pointerless value and do nothing. def emit_dec_ref( @@ -540,7 +544,10 @@ def emit_dec_ref( self.emit_line(f"CPy_{x}DecRef({dest});") else: # Inlined - self.emit_line(f"CPy_{x}DECREF({dest});") + if rtype.may_be_immortal or not HAVE_IMMORTAL: + self.emit_line(f"CPy_{x}DECREF({dest});") + else: + self.emit_line(f"CPy_{x}DECREF_NO_IMM({dest});") # Otherwise assume it's an unboxed, pointerless value and do nothing. def pretty_name(self, typ: RType) -> str: diff --git a/mypyc/codegen/emitfunc.py b/mypyc/codegen/emitfunc.py index 6088fb06dd32..7239e0835da0 100644 --- a/mypyc/codegen/emitfunc.py +++ b/mypyc/codegen/emitfunc.py @@ -7,6 +7,7 @@ from mypyc.analysis.blockfreq import frequently_executed_blocks from mypyc.codegen.emit import DEBUG_ERRORS, Emitter, TracebackAndGotoHandler, c_array_initializer from mypyc.common import ( + HAVE_IMMORTAL, MODULE_PREFIX, NATIVE_PREFIX, REG_PREFIX, @@ -76,9 +77,11 @@ RStruct, RTuple, RType, + is_bool_rprimitive, is_int32_rprimitive, is_int64_rprimitive, is_int_rprimitive, + is_none_rprimitive, is_pointer_rprimitive, is_tagged, ) @@ -578,6 +581,21 @@ def emit_method_call(self, dest: str, op_obj: Value, name: str, op_args: list[Va ) def visit_inc_ref(self, op: IncRef) -> None: + if ( + isinstance(op.src, Box) + and (is_none_rprimitive(op.src.src.type) or is_bool_rprimitive(op.src.src.type)) + and HAVE_IMMORTAL + ): + # On Python 3.12+, None/True/False are immortal, and we can skip inc ref + return + + if isinstance(op.src, LoadLiteral) and HAVE_IMMORTAL: + value = op.src.value + # We can skip inc ref for immortal literals on Python 3.12+ + if type(value) is int and -5 <= value <= 256: + # Small integers are immortal + return + src = self.reg(op.src) self.emit_inc_ref(src, op.src.type) diff --git a/mypyc/common.py b/mypyc/common.py index 724f61c34b78..c49952510c07 100644 --- a/mypyc/common.py +++ b/mypyc/common.py @@ -82,6 +82,12 @@ "pythonsupport.c", ] +# Python 3.12 introduced immortal objects, specified via a special reference count +# value. The reference counts of immortal objects are normally not modified, but it's +# not strictly wrong to modify them. See PEP 683 for more information, but note that +# some details in the PEP are out of date. +HAVE_IMMORTAL: Final = sys.version_info >= (3, 12) + JsonDict = dict[str, Any] diff --git a/mypyc/ir/rtypes.py b/mypyc/ir/rtypes.py index 96288423550c..6e7e94a618ab 100644 --- a/mypyc/ir/rtypes.py +++ b/mypyc/ir/rtypes.py @@ -26,7 +26,7 @@ from typing import TYPE_CHECKING, ClassVar, Final, Generic, TypeVar from typing_extensions import TypeGuard -from mypyc.common import IS_32_BIT_PLATFORM, PLATFORM_SIZE, JsonDict, short_name +from mypyc.common import HAVE_IMMORTAL, IS_32_BIT_PLATFORM, PLATFORM_SIZE, JsonDict, short_name from mypyc.namegen import NameGenerator if TYPE_CHECKING: @@ -69,6 +69,11 @@ def accept(self, visitor: RTypeVisitor[T]) -> T: def short_name(self) -> str: return short_name(self.name) + @property + @abstractmethod + def may_be_immortal(self) -> bool: + raise NotImplementedError + def __str__(self) -> str: return short_name(self.name) @@ -151,6 +156,10 @@ class RVoid(RType): def accept(self, visitor: RTypeVisitor[T]) -> T: return visitor.visit_rvoid(self) + @property + def may_be_immortal(self) -> bool: + return False + def serialize(self) -> str: return "void" @@ -193,6 +202,7 @@ def __init__( ctype: str = "PyObject *", size: int = PLATFORM_SIZE, error_overlap: bool = False, + may_be_immortal: bool = True, ) -> None: RPrimitive.primitive_map[name] = self @@ -204,6 +214,7 @@ def __init__( self._ctype = ctype self.size = size self.error_overlap = error_overlap + self._may_be_immortal = may_be_immortal and HAVE_IMMORTAL if ctype == "CPyTagged": self.c_undefined = "CPY_INT_TAG" elif ctype in ("int16_t", "int32_t", "int64_t"): @@ -230,6 +241,10 @@ def __init__( def accept(self, visitor: RTypeVisitor[T]) -> T: return visitor.visit_rprimitive(self) + @property + def may_be_immortal(self) -> bool: + return self._may_be_immortal + def serialize(self) -> str: return self.name @@ -433,8 +448,12 @@ def __hash__(self) -> int: "builtins.None", is_unboxed=True, is_refcounted=False, ctype="char", size=1 ) -# Python list object (or an instance of a subclass of list). -list_rprimitive: Final = RPrimitive("builtins.list", is_unboxed=False, is_refcounted=True) +# Python list object (or an instance of a subclass of list). These could be +# immortal, but since this is expected to be very rare, and the immortality checks +# can be pretty expensive for lists, we treat lists as non-immortal. +list_rprimitive: Final = RPrimitive( + "builtins.list", is_unboxed=False, is_refcounted=True, may_be_immortal=False +) # Python dict object (or an instance of a subclass of dict). dict_rprimitive: Final = RPrimitive("builtins.dict", is_unboxed=False, is_refcounted=True) @@ -642,6 +661,10 @@ def __init__(self, types: list[RType]) -> None: def accept(self, visitor: RTypeVisitor[T]) -> T: return visitor.visit_rtuple(self) + @property + def may_be_immortal(self) -> bool: + return False + def __str__(self) -> str: return "tuple[%s]" % ", ".join(str(typ) for typ in self.types) @@ -763,6 +786,10 @@ def __init__(self, name: str, names: list[str], types: list[RType]) -> None: def accept(self, visitor: RTypeVisitor[T]) -> T: return visitor.visit_rstruct(self) + @property + def may_be_immortal(self) -> bool: + return False + def __str__(self) -> str: # if not tuple(unnamed structs) return "{}{{{}}}".format( @@ -823,6 +850,10 @@ def __init__(self, class_ir: ClassIR) -> None: def accept(self, visitor: RTypeVisitor[T]) -> T: return visitor.visit_rinstance(self) + @property + def may_be_immortal(self) -> bool: + return False + def struct_name(self, names: NameGenerator) -> str: return self.class_ir.struct_name(names) @@ -883,6 +914,10 @@ def make_simplified_union(items: list[RType]) -> RType: def accept(self, visitor: RTypeVisitor[T]) -> T: return visitor.visit_runion(self) + @property + def may_be_immortal(self) -> bool: + return any(item.may_be_immortal for item in self.items) + def __repr__(self) -> str: return "" % ", ".join(str(item) for item in self.items) @@ -953,6 +988,10 @@ def __init__(self, item_type: RType, length: int) -> None: def accept(self, visitor: RTypeVisitor[T]) -> T: return visitor.visit_rarray(self) + @property + def may_be_immortal(self) -> bool: + return False + def __str__(self) -> str: return f"{self.item_type}[{self.length}]" diff --git a/mypyc/lib-rt/mypyc_util.h b/mypyc/lib-rt/mypyc_util.h index 9967f0a13b4f..01344331f04e 100644 --- a/mypyc/lib-rt/mypyc_util.h +++ b/mypyc/lib-rt/mypyc_util.h @@ -31,6 +31,35 @@ // Here just for consistency #define CPy_XDECREF(p) Py_XDECREF(p) +// The *_NO_IMM operations below perform refcount manipulation for +// non-immortal objects (Python 3.12 and later). +// +// Py_INCREF and other CPython operations check for immortality. This +// can be expensive when we know that an object cannot be immortal. + +static inline void CPy_INCREF_NO_IMM(PyObject *op) +{ + op->ob_refcnt++; +} + +static inline void CPy_DECREF_NO_IMM(PyObject *op) +{ + if (--op->ob_refcnt == 0) { + _Py_Dealloc(op); + } +} + +static inline void CPy_XDECREF_NO_IMM(PyObject *op) +{ + if (op != NULL && --op->ob_refcnt == 0) { + _Py_Dealloc(op); + } +} + +#define CPy_INCREF_NO_IMM(op) CPy_INCREF_NO_IMM((PyObject *)(op)) +#define CPy_DECREF_NO_IMM(op) CPy_DECREF_NO_IMM((PyObject *)(op)) +#define CPy_XDECREF_NO_IMM(op) CPy_XDECREF_NO_IMM((PyObject *)(op)) + // Tagged integer -- our representation of Python 'int' objects. // Small enough integers are represented as unboxed integers (shifted // left by 1); larger integers (larger than 63 bits on a 64-bit diff --git a/mypyc/test/test_emit.py b/mypyc/test/test_emit.py index e4ace3ec01f0..1baed3964299 100644 --- a/mypyc/test/test_emit.py +++ b/mypyc/test/test_emit.py @@ -3,8 +3,21 @@ import unittest from mypyc.codegen.emit import Emitter, EmitterContext +from mypyc.common import HAVE_IMMORTAL +from mypyc.ir.class_ir import ClassIR from mypyc.ir.ops import BasicBlock, Register, Value -from mypyc.ir.rtypes import RTuple, bool_rprimitive, int_rprimitive, str_rprimitive +from mypyc.ir.rtypes import ( + RInstance, + RTuple, + RUnion, + bool_rprimitive, + int_rprimitive, + list_rprimitive, + none_rprimitive, + object_rprimitive, + str_rprimitive, +) +from mypyc.irbuild.vtable import compute_vtable from mypyc.namegen import NameGenerator @@ -12,10 +25,15 @@ class TestEmitter(unittest.TestCase): def setUp(self) -> None: self.n = Register(int_rprimitive, "n") self.context = EmitterContext(NameGenerator([["mod"]])) + self.emitter = Emitter(self.context, {}) + + ir = ClassIR("A", "mod") + compute_vtable(ir) + ir.mro = [ir] + self.instance_a = RInstance(ir) def test_label(self) -> None: - emitter = Emitter(self.context, {}) - assert emitter.label(BasicBlock(4)) == "CPyL4" + assert self.emitter.label(BasicBlock(4)) == "CPyL4" def test_reg(self) -> None: names: dict[Value, str] = {self.n: "n"} @@ -23,17 +41,16 @@ def test_reg(self) -> None: assert emitter.reg(self.n) == "cpy_r_n" def test_object_annotation(self) -> None: - emitter = Emitter(self.context, {}) - assert emitter.object_annotation("hello, world", "line;") == " /* 'hello, world' */" + assert self.emitter.object_annotation("hello, world", "line;") == " /* 'hello, world' */" assert ( - emitter.object_annotation(list(range(30)), "line;") + self.emitter.object_annotation(list(range(30)), "line;") == """\ /* [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29] */""" ) def test_emit_line(self) -> None: - emitter = Emitter(self.context, {}) + emitter = self.emitter emitter.emit_line("line;") emitter.emit_line("a {") emitter.emit_line("f();") @@ -51,13 +68,13 @@ def test_emit_line(self) -> None: ) def test_emit_undefined_value_for_simple_type(self) -> None: - emitter = Emitter(self.context, {}) + emitter = self.emitter assert emitter.c_undefined_value(int_rprimitive) == "CPY_INT_TAG" assert emitter.c_undefined_value(str_rprimitive) == "NULL" assert emitter.c_undefined_value(bool_rprimitive) == "2" def test_emit_undefined_value_for_tuple(self) -> None: - emitter = Emitter(self.context, {}) + emitter = self.emitter assert ( emitter.c_undefined_value(RTuple([str_rprimitive, int_rprimitive, bool_rprimitive])) == "(tuple_T3OIC) { NULL, CPY_INT_TAG, 2 }" @@ -67,3 +84,87 @@ def test_emit_undefined_value_for_tuple(self) -> None: emitter.c_undefined_value(RTuple([RTuple([str_rprimitive]), bool_rprimitive])) == "(tuple_T2T1OC) { { NULL }, 2 }" ) + + def test_emit_inc_ref_object(self) -> None: + self.emitter.emit_inc_ref("x", object_rprimitive) + self.assert_output("CPy_INCREF(x);\n") + + def test_emit_inc_ref_int(self) -> None: + self.emitter.emit_inc_ref("x", int_rprimitive) + self.assert_output("CPyTagged_INCREF(x);\n") + + def test_emit_inc_ref_rare(self) -> None: + self.emitter.emit_inc_ref("x", object_rprimitive, rare=True) + self.assert_output("CPy_INCREF(x);\n") + self.emitter.emit_inc_ref("x", int_rprimitive, rare=True) + self.assert_output("CPyTagged_IncRef(x);\n") + + def test_emit_inc_ref_list(self) -> None: + self.emitter.emit_inc_ref("x", list_rprimitive) + if HAVE_IMMORTAL: + self.assert_output("CPy_INCREF_NO_IMM(x);\n") + else: + self.assert_output("CPy_INCREF(x);\n") + + def test_emit_inc_ref_instance(self) -> None: + self.emitter.emit_inc_ref("x", self.instance_a) + if HAVE_IMMORTAL: + self.assert_output("CPy_INCREF_NO_IMM(x);\n") + else: + self.assert_output("CPy_INCREF(x);\n") + + def test_emit_inc_ref_optional(self) -> None: + optional = RUnion([self.instance_a, none_rprimitive]) + self.emitter.emit_inc_ref("o", optional) + self.assert_output("CPy_INCREF(o);\n") + + def test_emit_dec_ref_object(self) -> None: + self.emitter.emit_dec_ref("x", object_rprimitive) + self.assert_output("CPy_DECREF(x);\n") + self.emitter.emit_dec_ref("x", object_rprimitive, is_xdec=True) + self.assert_output("CPy_XDECREF(x);\n") + + def test_emit_dec_ref_int(self) -> None: + self.emitter.emit_dec_ref("x", int_rprimitive) + self.assert_output("CPyTagged_DECREF(x);\n") + self.emitter.emit_dec_ref("x", int_rprimitive, is_xdec=True) + self.assert_output("CPyTagged_XDECREF(x);\n") + + def test_emit_dec_ref_rare(self) -> None: + self.emitter.emit_dec_ref("x", object_rprimitive, rare=True) + self.assert_output("CPy_DecRef(x);\n") + self.emitter.emit_dec_ref("x", int_rprimitive, rare=True) + self.assert_output("CPyTagged_DecRef(x);\n") + + def test_emit_dec_ref_list(self) -> None: + self.emitter.emit_dec_ref("x", list_rprimitive) + if HAVE_IMMORTAL: + self.assert_output("CPy_DECREF_NO_IMM(x);\n") + else: + self.assert_output("CPy_DECREF(x);\n") + self.emitter.emit_dec_ref("x", list_rprimitive, is_xdec=True) + if HAVE_IMMORTAL: + self.assert_output("CPy_XDECREF_NO_IMM(x);\n") + else: + self.assert_output("CPy_XDECREF(x);\n") + + def test_emit_dec_ref_instance(self) -> None: + self.emitter.emit_dec_ref("x", self.instance_a) + if HAVE_IMMORTAL: + self.assert_output("CPy_DECREF_NO_IMM(x);\n") + else: + self.assert_output("CPy_DECREF(x);\n") + self.emitter.emit_dec_ref("x", self.instance_a, is_xdec=True) + if HAVE_IMMORTAL: + self.assert_output("CPy_XDECREF_NO_IMM(x);\n") + else: + self.assert_output("CPy_XDECREF(x);\n") + + def test_emit_dec_ref_optional(self) -> None: + optional = RUnion([self.instance_a, none_rprimitive]) + self.emitter.emit_dec_ref("o", optional) + self.assert_output("CPy_DECREF(o);\n") + + def assert_output(self, expected: str) -> None: + assert "".join(self.emitter.fragments) == expected + self.emitter.fragments = [] diff --git a/mypyc/test/test_emitfunc.py b/mypyc/test/test_emitfunc.py index 90df131288f9..275e8c383a4b 100644 --- a/mypyc/test/test_emitfunc.py +++ b/mypyc/test/test_emitfunc.py @@ -5,7 +5,7 @@ from mypy.test.helpers import assert_string_arrays_equal from mypyc.codegen.emit import Emitter, EmitterContext from mypyc.codegen.emitfunc import FunctionEmitterVisitor, generate_native_function -from mypyc.common import PLATFORM_SIZE +from mypyc.common import HAVE_IMMORTAL, PLATFORM_SIZE from mypyc.ir.class_ir import ClassIR from mypyc.ir.func_ir import FuncDecl, FuncIR, FuncSignature, RuntimeArg from mypyc.ir.ops import ( @@ -28,6 +28,7 @@ Integer, IntOp, LoadAddress, + LoadLiteral, LoadMem, Op, Register, @@ -53,6 +54,7 @@ int64_rprimitive, int_rprimitive, list_rprimitive, + none_rprimitive, object_rprimitive, pointer_rprimitive, short_int_rprimitive, @@ -114,6 +116,7 @@ def add_local(name: str, rtype: RType) -> Register: compute_vtable(ir) ir.mro = [ir] self.r = add_local("r", RInstance(ir)) + self.none = add_local("none", none_rprimitive) self.context = EmitterContext(NameGenerator([["mod"]])) @@ -805,9 +808,25 @@ def test_extend(self) -> None: Extend(a, int_rprimitive, signed=False), """cpy_r_r0 = (uint32_t)cpy_r_a;""" ) + def test_inc_ref_none(self) -> None: + b = Box(self.none) + self.assert_emit([b, IncRef(b)], "" if HAVE_IMMORTAL else "CPy_INCREF(cpy_r_r0);") + + def test_inc_ref_bool(self) -> None: + b = Box(self.b) + self.assert_emit([b, IncRef(b)], "" if HAVE_IMMORTAL else "CPy_INCREF(cpy_r_r0);") + + def test_inc_ref_int_literal(self) -> None: + for x in -5, 0, 1, 5, 255, 256: + b = LoadLiteral(x, object_rprimitive) + self.assert_emit([b, IncRef(b)], "" if HAVE_IMMORTAL else "CPy_INCREF(cpy_r_r0);") + for x in -1123355, -6, 257, 123235345: + b = LoadLiteral(x, object_rprimitive) + self.assert_emit([b, IncRef(b)], "CPy_INCREF(cpy_r_r0);") + def assert_emit( self, - op: Op, + op: Op | list[Op], expected: str, next_block: BasicBlock | None = None, *, @@ -816,7 +835,11 @@ def assert_emit( skip_next: bool = False, ) -> None: block = BasicBlock(0) - block.ops.append(op) + if isinstance(op, Op): + block.ops.append(op) + else: + block.ops.extend(op) + op = op[-1] value_names = generate_names_for_ir(self.registers, [block]) emitter = Emitter(self.context, value_names) declarations = Emitter(self.context, value_names)