From 9cc9cb0a769046380dd61d8b21904a794e30cc4f Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 2 Jan 2025 16:16:59 +0000 Subject: [PATCH 01/16] WIP incref operations that don't check for immortality --- mypyc/lib-rt/mypyc_util.h | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/mypyc/lib-rt/mypyc_util.h b/mypyc/lib-rt/mypyc_util.h index 9967f0a13b4f3..c7d07c258f2dc 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 are refcount manipulation operations 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 From b4d441e7e8cd17addd73c3e5637fdc0c48faabfb Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 2 Jan 2025 16:40:09 +0000 Subject: [PATCH 02/16] WIP use more efficient refcount ops Fix --- mypyc/codegen/emit.py | 12 ++++++++--- mypyc/ir/rtypes.py | 44 ++++++++++++++++++++++++++++++++++++--- mypyc/lib-rt/mypyc_util.h | 2 +- 3 files changed, 51 insertions(+), 7 deletions(-) diff --git a/mypyc/codegen/emit.py b/mypyc/codegen/emit.py index f6663e6194dc3..c7017cc2d9169 100644 --- a/mypyc/codegen/emit.py +++ b/mypyc/codegen/emit.py @@ -511,8 +511,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: + 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 +543,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: + 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/ir/rtypes.py b/mypyc/ir/rtypes.py index 96288423550c8..fabdaead878cd 100644 --- a/mypyc/ir/rtypes.py +++ b/mypyc/ir/rtypes.py @@ -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(sel) -> 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 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 @@ -434,13 +449,16 @@ def __hash__(self) -> int: ) # Python list object (or an instance of a subclass of list). -list_rprimitive: Final = RPrimitive("builtins.list", is_unboxed=False, is_refcounted=True) +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) +dict_rprimitive: Final = RPrimitive("builtins.dict", is_unboxed=False, is_refcounted=True, + may_be_immortal=False) # Python set object (or an instance of a subclass of set). -set_rprimitive: Final = RPrimitive("builtins.set", is_unboxed=False, is_refcounted=True) +set_rprimitive: Final = RPrimitive("builtins.set", is_unboxed=False, is_refcounted=True, + may_be_immortal=False) # Python str object. At the C layer, str is referred to as unicode # (PyUnicode). @@ -642,6 +660,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 +785,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 +849,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 +913,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 +987,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 c7d07c258f2dc..01344331f04e0 100644 --- a/mypyc/lib-rt/mypyc_util.h +++ b/mypyc/lib-rt/mypyc_util.h @@ -31,7 +31,7 @@ // Here just for consistency #define CPy_XDECREF(p) Py_XDECREF(p) -// The *_NO_IMM operations below are refcount manipulation operations for +// 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 From 4e27e8020c8c749513f310080bfa06eb1e7974ad Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 2 Jan 2025 17:06:49 +0000 Subject: [PATCH 03/16] Don't increase None refcount --- mypyc/codegen/emitfunc.py | 6 ++++++ mypyc/common.py | 3 +++ 2 files changed, 9 insertions(+) diff --git a/mypyc/codegen/emitfunc.py b/mypyc/codegen/emitfunc.py index 6088fb06dd32f..160b5dcef07d7 100644 --- a/mypyc/codegen/emitfunc.py +++ b/mypyc/codegen/emitfunc.py @@ -13,6 +13,7 @@ STATIC_PREFIX, TYPE_PREFIX, TYPE_VAR_PREFIX, + HAVE_IMMORTAL, ) from mypyc.ir.class_ir import ClassIR from mypyc.ir.func_ir import FUNC_CLASSMETHOD, FUNC_STATICMETHOD, FuncDecl, FuncIR, all_values @@ -81,6 +82,7 @@ is_int_rprimitive, is_pointer_rprimitive, is_tagged, + is_none_rprimitive, ) @@ -578,6 +580,10 @@ 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) and HAVE_IMMORTAL: + # On Python 3.12+, None is immortal and needs no reference count manipulation. + 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 724f61c34b78f..20a2e6c841793 100644 --- a/mypyc/common.py +++ b/mypyc/common.py @@ -82,6 +82,9 @@ "pythonsupport.c", ] +# Python 3.12 introduced immortal objects, specified via a special reference count value +HAVE_IMMORTAL: Final = sys.version_info >= (3, 12) + JsonDict = dict[str, Any] From 1a98389145fbb4098feb11e7a22f9fc4a9c6a6d8 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 2 Jan 2025 17:40:22 +0000 Subject: [PATCH 04/16] Don't inc ref bool and small integer values --- mypyc/codegen/emitfunc.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/mypyc/codegen/emitfunc.py b/mypyc/codegen/emitfunc.py index 160b5dcef07d7..ce34e5e6c9b82 100644 --- a/mypyc/codegen/emitfunc.py +++ b/mypyc/codegen/emitfunc.py @@ -83,6 +83,7 @@ is_pointer_rprimitive, is_tagged, is_none_rprimitive, + is_bool_rprimitive, ) @@ -580,10 +581,18 @@ 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) and HAVE_IMMORTAL: - # On Python 3.12+, None is immortal and needs no reference count manipulation. + 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) From 5fcc9c9466674d5ab6409b37cfe16d7e1be5e4ce Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Fri, 3 Jan 2025 12:19:58 +0000 Subject: [PATCH 05/16] Optimized decref for optional types --- mypyc/codegen/emit.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/mypyc/codegen/emit.py b/mypyc/codegen/emit.py index c7017cc2d9169..e5c038b339091 100644 --- a/mypyc/codegen/emit.py +++ b/mypyc/codegen/emit.py @@ -17,6 +17,7 @@ STATIC_PREFIX, TYPE_PREFIX, use_vectorcall, + HAVE_IMMORTAL, ) from mypyc.ir.class_ir import ClassIR, all_concrete_classes from mypyc.ir.func_ir import FuncDecl @@ -544,7 +545,14 @@ def emit_dec_ref( else: # Inlined if rtype.may_be_immortal: - self.emit_line(f"CPy_{x}DECREF({dest});") + if HAVE_IMMORTAL and (opt := optional_value_type(rtype)) and not opt.may_be_immortal: + # Optimized decref of optional type avoids reading reference count + # if value is None (None is immortal) + self.emit_line(f"if ({dest} != Py_None) {{") + self.emit_line(f"CPy_{x}DECREF_NO_IMM({dest});") + self.emit_line("}") + else: + 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. From 37605f62e5c4730ba1509591547865287f0fb075 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 13 Jan 2025 13:21:57 +0000 Subject: [PATCH 06/16] Dict and set objects may be immortal For example, module namespace dictionaries could reasonably be immortal. Still treat lists as non-immportal, since the performance hit be too high otherwise. --- mypyc/ir/rtypes.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mypyc/ir/rtypes.py b/mypyc/ir/rtypes.py index fabdaead878cd..31ddfde77aaac 100644 --- a/mypyc/ir/rtypes.py +++ b/mypyc/ir/rtypes.py @@ -448,17 +448,17 @@ 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). +# 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, - may_be_immortal=False) +dict_rprimitive: Final = RPrimitive("builtins.dict", is_unboxed=False, is_refcounted=True) # Python set object (or an instance of a subclass of set). -set_rprimitive: Final = RPrimitive("builtins.set", is_unboxed=False, is_refcounted=True, - may_be_immortal=False) +set_rprimitive: Final = RPrimitive("builtins.set", is_unboxed=False, is_refcounted=True) # Python str object. At the C layer, str is referred to as unicode # (PyUnicode). From b864f89c99202c09c5f1c6a913242d4c413c0a1a Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 13 Jan 2025 13:23:53 +0000 Subject: [PATCH 07/16] Update comment --- mypyc/common.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mypyc/common.py b/mypyc/common.py index 20a2e6c841793..c49952510c078 100644 --- a/mypyc/common.py +++ b/mypyc/common.py @@ -82,7 +82,10 @@ "pythonsupport.c", ] -# Python 3.12 introduced immortal objects, specified via a special reference count value +# 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) From 21db06dae9460a183867cf0d2f9ee1e63a4f3375 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 13 Jan 2025 13:33:46 +0000 Subject: [PATCH 08/16] Nothing can be immortal on pre-3.12 Python versions --- mypyc/ir/rtypes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mypyc/ir/rtypes.py b/mypyc/ir/rtypes.py index 31ddfde77aaac..97e1bcb72d3d7 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: @@ -214,7 +214,7 @@ def __init__( self._ctype = ctype self.size = size self.error_overlap = error_overlap - self._may_be_immortal = may_be_immortal + 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"): From 66d5a652e4644028fac222a89b1af02ebe05f8fd Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 13 Jan 2025 13:55:06 +0000 Subject: [PATCH 09/16] Lint --- mypyc/codegen/emit.py | 8 ++++++-- mypyc/codegen/emitfunc.py | 13 ++++++++----- mypyc/ir/rtypes.py | 7 ++++--- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/mypyc/codegen/emit.py b/mypyc/codegen/emit.py index e5c038b339091..36658987f91ad 100644 --- a/mypyc/codegen/emit.py +++ b/mypyc/codegen/emit.py @@ -12,12 +12,12 @@ ATTR_PREFIX, BITMAP_BITS, FAST_ISINSTANCE_MAX_SUBCLASSES, + HAVE_IMMORTAL, NATIVE_PREFIX, REG_PREFIX, STATIC_PREFIX, TYPE_PREFIX, use_vectorcall, - HAVE_IMMORTAL, ) from mypyc.ir.class_ir import ClassIR, all_concrete_classes from mypyc.ir.func_ir import FuncDecl @@ -545,7 +545,11 @@ def emit_dec_ref( else: # Inlined if rtype.may_be_immortal: - if HAVE_IMMORTAL and (opt := optional_value_type(rtype)) and not opt.may_be_immortal: + if ( + HAVE_IMMORTAL + and (opt := optional_value_type(rtype)) + and not opt.may_be_immortal + ): # Optimized decref of optional type avoids reading reference count # if value is None (None is immortal) self.emit_line(f"if ({dest} != Py_None) {{") diff --git a/mypyc/codegen/emitfunc.py b/mypyc/codegen/emitfunc.py index ce34e5e6c9b82..7239e0835da04 100644 --- a/mypyc/codegen/emitfunc.py +++ b/mypyc/codegen/emitfunc.py @@ -7,13 +7,13 @@ 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, STATIC_PREFIX, TYPE_PREFIX, TYPE_VAR_PREFIX, - HAVE_IMMORTAL, ) from mypyc.ir.class_ir import ClassIR from mypyc.ir.func_ir import FUNC_CLASSMETHOD, FUNC_STATICMETHOD, FuncDecl, FuncIR, all_values @@ -77,13 +77,13 @@ RStruct, RTuple, RType, + is_bool_rprimitive, is_int32_rprimitive, is_int64_rprimitive, is_int_rprimitive, + is_none_rprimitive, is_pointer_rprimitive, is_tagged, - is_none_rprimitive, - is_bool_rprimitive, ) @@ -581,8 +581,11 @@ 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: + 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 diff --git a/mypyc/ir/rtypes.py b/mypyc/ir/rtypes.py index 97e1bcb72d3d7..6e7e94a618abf 100644 --- a/mypyc/ir/rtypes.py +++ b/mypyc/ir/rtypes.py @@ -71,7 +71,7 @@ def short_name(self) -> str: @property @abstractmethod - def may_be_immortal(sel) -> bool: + def may_be_immortal(self) -> bool: raise NotImplementedError def __str__(self) -> str: @@ -451,8 +451,9 @@ def __hash__(self) -> int: # 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) +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) From ec4be73dfd810122638bafb7e6ecef9b62bde980 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 13 Jan 2025 14:08:41 +0000 Subject: [PATCH 10/16] Fix pre-3.12 Python version and add tests --- mypyc/codegen/emit.py | 4 +-- mypyc/test/test_emit.py | 80 ++++++++++++++++++++++++++++++++++++----- 2 files changed, 73 insertions(+), 11 deletions(-) diff --git a/mypyc/codegen/emit.py b/mypyc/codegen/emit.py index 36658987f91ad..8176260381f2e 100644 --- a/mypyc/codegen/emit.py +++ b/mypyc/codegen/emit.py @@ -513,7 +513,7 @@ def emit_inc_ref(self, dest: str, rtype: RType, *, rare: bool = False) -> None: self.emit_inc_ref(f"{dest}.f{i}", item_type) elif not rtype.is_unboxed: # Always inline, since this is a simple but very hot op - if rtype.may_be_immortal: + 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) @@ -544,7 +544,7 @@ def emit_dec_ref( self.emit_line(f"CPy_{x}DecRef({dest});") else: # Inlined - if rtype.may_be_immortal: + if rtype.may_be_immortal or not HAVE_IMMORTAL: if ( HAVE_IMMORTAL and (opt := optional_value_type(rtype)) diff --git a/mypyc/test/test_emit.py b/mypyc/test/test_emit.py index e4ace3ec01f02..3e617ef38e31c 100644 --- a/mypyc/test/test_emit.py +++ b/mypyc/test/test_emit.py @@ -3,8 +3,16 @@ import unittest from mypyc.codegen.emit import Emitter, EmitterContext +from mypyc.common import HAVE_IMMORTAL 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 ( + RTuple, + bool_rprimitive, + int_rprimitive, + list_rprimitive, + object_rprimitive, + str_rprimitive, +) from mypyc.namegen import NameGenerator @@ -12,10 +20,10 @@ class TestEmitter(unittest.TestCase): def setUp(self) -> None: self.n = Register(int_rprimitive, "n") self.context = EmitterContext(NameGenerator([["mod"]])) + self.emitter = Emitter(self.context, {}) 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 +31,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 +58,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 +74,58 @@ 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_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 assert_output(self, expected: str) -> None: + assert "".join(self.emitter.fragments) == expected + self.emitter.fragments = [] From 9704c843778ff6de97c2c95fe9b6a9d5f0f99b9e Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 13 Jan 2025 14:25:35 +0000 Subject: [PATCH 11/16] More tests --- mypyc/test/test_emit.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/mypyc/test/test_emit.py b/mypyc/test/test_emit.py index 3e617ef38e31c..816f9b59188fd 100644 --- a/mypyc/test/test_emit.py +++ b/mypyc/test/test_emit.py @@ -4,8 +4,10 @@ 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 ( + RInstance, RTuple, bool_rprimitive, int_rprimitive, @@ -13,6 +15,7 @@ object_rprimitive, str_rprimitive, ) +from mypyc.irbuild.vtable import compute_vtable from mypyc.namegen import NameGenerator @@ -22,6 +25,11 @@ def setUp(self) -> None: 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: assert self.emitter.label(BasicBlock(4)) == "CPyL4" @@ -96,6 +104,13 @@ def test_emit_inc_ref_list(self) -> None: 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_dec_ref_object(self) -> None: self.emitter.emit_dec_ref("x", object_rprimitive) self.assert_output("CPy_DECREF(x);\n") @@ -126,6 +141,18 @@ def test_emit_dec_ref_list(self) -> None: 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 assert_output(self, expected: str) -> None: assert "".join(self.emitter.fragments) == expected self.emitter.fragments = [] From 3a96b7c5108ac65735a3ceb176bc11ef797d07c1 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 13 Jan 2025 14:34:05 +0000 Subject: [PATCH 12/16] Add test case --- mypyc/test/test_emit.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/mypyc/test/test_emit.py b/mypyc/test/test_emit.py index 816f9b59188fd..f2818bca0211b 100644 --- a/mypyc/test/test_emit.py +++ b/mypyc/test/test_emit.py @@ -9,9 +9,11 @@ from mypyc.ir.rtypes import ( RInstance, RTuple, + RUnion, bool_rprimitive, int_rprimitive, list_rprimitive, + none_rprimitive, object_rprimitive, str_rprimitive, ) @@ -153,6 +155,14 @@ def test_emit_dec_ref_instance(self) -> None: 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) + if HAVE_IMMORTAL: + self.assert_output("if (o != Py_None) {\n CPy_DECREF_NO_IMM(o);\n}\n") + else: + self.assert_output("CPy_DECREF(o);\n") + def assert_output(self, expected: str) -> None: assert "".join(self.emitter.fragments) == expected self.emitter.fragments = [] From fdc6e7b06055ecd05bcc46356cbf3caf204658ae Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 13 Jan 2025 14:39:27 +0000 Subject: [PATCH 13/16] Optimize inc ref of optional types slightly --- mypyc/codegen/emit.py | 13 ++++++++++++- mypyc/test/test_emit.py | 19 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/mypyc/codegen/emit.py b/mypyc/codegen/emit.py index 8176260381f2e..62f8d87dfc569 100644 --- a/mypyc/codegen/emit.py +++ b/mypyc/codegen/emit.py @@ -514,7 +514,18 @@ def emit_inc_ref(self, dest: str, rtype: RType, *, rare: bool = False) -> None: elif not rtype.is_unboxed: # 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) + if ( + HAVE_IMMORTAL + and (opt := optional_value_type(rtype)) + and not opt.may_be_immortal + ): + # Optimized incref of optional type avoids reading reference count + # if value is None (None is immortal) + self.emit_line(f"if ({dest} != Py_None) {{") + self.emit_line(f"CPy_INCREF_NO_IMM({dest});") + self.emit_line("}") + else: + 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. diff --git a/mypyc/test/test_emit.py b/mypyc/test/test_emit.py index f2818bca0211b..587739e436c11 100644 --- a/mypyc/test/test_emit.py +++ b/mypyc/test/test_emit.py @@ -11,6 +11,7 @@ RTuple, RUnion, bool_rprimitive, + dict_rprimitive, int_rprimitive, list_rprimitive, none_rprimitive, @@ -113,6 +114,19 @@ def test_emit_inc_ref_instance(self) -> None: 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) + if HAVE_IMMORTAL: + self.assert_output("if (o != Py_None) {\n CPy_INCREF_NO_IMM(o);\n}\n") + else: + self.assert_output("CPy_INCREF(o);\n") + + def test_emit_inc_ref_optional_2(self) -> None: + optional = RUnion([dict_rprimitive, 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") @@ -163,6 +177,11 @@ def test_emit_dec_ref_optional(self) -> None: else: self.assert_output("CPy_DECREF(o);\n") + def test_emit_dec_ref_optional_2(self) -> None: + optional = RUnion([dict_rprimitive, 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 = [] From a1782de23fc8c969e1b2ed9cc98b4931b45b4e99 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 13 Jan 2025 14:54:56 +0000 Subject: [PATCH 14/16] Revert optional type special casing I'm not convinced that it's any better. Getting the address of None requires a memory read, so we'd have an extra memory read in the slow path. --- mypyc/codegen/emit.py | 26 ++------------------------ mypyc/test/test_emit.py | 17 ----------------- 2 files changed, 2 insertions(+), 41 deletions(-) diff --git a/mypyc/codegen/emit.py b/mypyc/codegen/emit.py index 62f8d87dfc569..bef560b3d42ad 100644 --- a/mypyc/codegen/emit.py +++ b/mypyc/codegen/emit.py @@ -514,18 +514,7 @@ def emit_inc_ref(self, dest: str, rtype: RType, *, rare: bool = False) -> None: elif not rtype.is_unboxed: # Always inline, since this is a simple but very hot op if rtype.may_be_immortal or not HAVE_IMMORTAL: - if ( - HAVE_IMMORTAL - and (opt := optional_value_type(rtype)) - and not opt.may_be_immortal - ): - # Optimized incref of optional type avoids reading reference count - # if value is None (None is immortal) - self.emit_line(f"if ({dest} != Py_None) {{") - self.emit_line(f"CPy_INCREF_NO_IMM({dest});") - self.emit_line("}") - else: - self.emit_line("CPy_INCREF(%s);" % dest) + 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. @@ -556,18 +545,7 @@ def emit_dec_ref( else: # Inlined if rtype.may_be_immortal or not HAVE_IMMORTAL: - if ( - HAVE_IMMORTAL - and (opt := optional_value_type(rtype)) - and not opt.may_be_immortal - ): - # Optimized decref of optional type avoids reading reference count - # if value is None (None is immortal) - self.emit_line(f"if ({dest} != Py_None) {{") - self.emit_line(f"CPy_{x}DECREF_NO_IMM({dest});") - self.emit_line("}") - else: - self.emit_line(f"CPy_{x}DECREF({dest});") + 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. diff --git a/mypyc/test/test_emit.py b/mypyc/test/test_emit.py index 587739e436c11..1baed3964299e 100644 --- a/mypyc/test/test_emit.py +++ b/mypyc/test/test_emit.py @@ -11,7 +11,6 @@ RTuple, RUnion, bool_rprimitive, - dict_rprimitive, int_rprimitive, list_rprimitive, none_rprimitive, @@ -117,14 +116,6 @@ def test_emit_inc_ref_instance(self) -> None: def test_emit_inc_ref_optional(self) -> None: optional = RUnion([self.instance_a, none_rprimitive]) self.emitter.emit_inc_ref("o", optional) - if HAVE_IMMORTAL: - self.assert_output("if (o != Py_None) {\n CPy_INCREF_NO_IMM(o);\n}\n") - else: - self.assert_output("CPy_INCREF(o);\n") - - def test_emit_inc_ref_optional_2(self) -> None: - optional = RUnion([dict_rprimitive, none_rprimitive]) - self.emitter.emit_inc_ref("o", optional) self.assert_output("CPy_INCREF(o);\n") def test_emit_dec_ref_object(self) -> None: @@ -172,14 +163,6 @@ def test_emit_dec_ref_instance(self) -> None: def test_emit_dec_ref_optional(self) -> None: optional = RUnion([self.instance_a, none_rprimitive]) self.emitter.emit_dec_ref("o", optional) - if HAVE_IMMORTAL: - self.assert_output("if (o != Py_None) {\n CPy_DECREF_NO_IMM(o);\n}\n") - else: - self.assert_output("CPy_DECREF(o);\n") - - def test_emit_dec_ref_optional_2(self) -> None: - optional = RUnion([dict_rprimitive, none_rprimitive]) - self.emitter.emit_dec_ref("o", optional) self.assert_output("CPy_DECREF(o);\n") def assert_output(self, expected: str) -> None: From 67f33ddd0ec13580158105751ed2719a5f3ee2cf Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 13 Jan 2025 15:54:32 +0000 Subject: [PATCH 15/16] Add more incref tests --- mypyc/test/test_emitfunc.py | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/mypyc/test/test_emitfunc.py b/mypyc/test/test_emitfunc.py index 90df131288f98..afe6c1b4a4608 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 -6, 257: + 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) From bbb136810952f8f5d7f28ea7b31b89b92ba2d5fe Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 13 Jan 2025 15:55:23 +0000 Subject: [PATCH 16/16] Tweak test case --- mypyc/test/test_emitfunc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypyc/test/test_emitfunc.py b/mypyc/test/test_emitfunc.py index afe6c1b4a4608..275e8c383a4b5 100644 --- a/mypyc/test/test_emitfunc.py +++ b/mypyc/test/test_emitfunc.py @@ -820,7 +820,7 @@ 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 -6, 257: + for x in -1123355, -6, 257, 123235345: b = LoadLiteral(x, object_rprimitive) self.assert_emit([b, IncRef(b)], "CPy_INCREF(cpy_r_r0);")