From a1404fccf0ad7b8be47c9dbc51139c4da9098287 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 1 Sep 2025 16:36:50 +0100 Subject: [PATCH 1/3] [mypyc] Allow defining a single-item free "list" for a native class It's quite common to have a class where we almost always have at most a single allocated instance (per thread). Now these instances can be allocated more quickly, by reusing the memory that was used for the most recently freed instance (a separate memory block is reused for each thread on free-threaded builds). It's used like this (only the value 1 is supported for now): ``` from mypy_extensions import mypyc_attr @mypyc_attr(free_list=1) class Foo: ... ``` This makes a microbenchmark that only allocates and immediately frees simple objects repeatedly around 3.8x faster. It's probably worth extending this to support larger free lists in the future. We can later look into enabling this automatically for certain native classes based on profile information. --- mypyc/irbuild/prepare.py | 16 ++++++++++++- mypyc/irbuild/util.py | 9 ++++++-- mypyc/test-data/irbuild-classes.test | 20 ++++++++++++++++ mypyc/test-data/run-classes.test | 34 ++++++++++++++++++++++++++++ 4 files changed, 76 insertions(+), 3 deletions(-) diff --git a/mypyc/irbuild/prepare.py b/mypyc/irbuild/prepare.py index 95c8c448d642c..008d78f6e9a05 100644 --- a/mypyc/irbuild/prepare.py +++ b/mypyc/irbuild/prepare.py @@ -351,13 +351,27 @@ def prepare_class_def( ir = mapper.type_to_ir[cdef.info] info = cdef.info - attrs = get_mypyc_attrs(cdef) + attrs, attrs_lines = get_mypyc_attrs(cdef) if attrs.get("allow_interpreted_subclasses") is True: ir.allow_interpreted_subclasses = True if attrs.get("serializable") is True: # Supports copy.copy and pickle (including subclasses) ir._serializable = True + free_list_len = attrs.get("free_list_len") + if free_list_len is not None: + line = attrs_lines["free_list_len"] + if ir.is_trait: + errors.error('"free_list_len" can\'t be used with traits', path, line) + if free_list_len == 1: + ir.reuse_freed_instance = True + else: + errors.error( + f'Unsupported value for "free_list_len": {free_list_len}', + path, + line, + ) + # Check for subclassing from builtin types for cls in info.mro: # Special case exceptions and dicts diff --git a/mypyc/irbuild/util.py b/mypyc/irbuild/util.py index 757b49c68c83b..eca2cac7e9dba 100644 --- a/mypyc/irbuild/util.py +++ b/mypyc/irbuild/util.py @@ -96,6 +96,8 @@ def get_mypyc_attr_literal(e: Expression) -> Any: return False elif isinstance(e, RefExpr) and e.fullname == "builtins.None": return None + elif isinstance(e, IntExpr): + return e.value return NotImplemented @@ -110,9 +112,10 @@ def get_mypyc_attr_call(d: Expression) -> CallExpr | None: return None -def get_mypyc_attrs(stmt: ClassDef | Decorator) -> dict[str, Any]: +def get_mypyc_attrs(stmt: ClassDef | Decorator) -> tuple[dict[str, Any], dict[str, int]]: """Collect all the mypyc_attr attributes on a class definition or a function.""" attrs: dict[str, Any] = {} + lines: dict[str, int] = {} for dec in stmt.decorators: d = get_mypyc_attr_call(dec) if d: @@ -120,10 +123,12 @@ def get_mypyc_attrs(stmt: ClassDef | Decorator) -> dict[str, Any]: if name is None: if isinstance(arg, StrExpr): attrs[arg.value] = True + lines[arg.value] = d.line else: attrs[name] = get_mypyc_attr_literal(arg) + lines[name] = d.line - return attrs + return attrs, lines def is_extension_class(path: str, cdef: ClassDef, errors: Errors) -> bool: diff --git a/mypyc/test-data/irbuild-classes.test b/mypyc/test-data/irbuild-classes.test index bb55958dc6dcc..2f59bb000220e 100644 --- a/mypyc/test-data/irbuild-classes.test +++ b/mypyc/test-data/irbuild-classes.test @@ -1734,3 +1734,23 @@ class NonNative: class InheritsPython(dict): def __new__(cls) -> InheritsPython: return super().__new__(cls) # E: super().__new__() not supported for classes inheriting from non-native classes + +[case testClassWithFreeList] +from mypy_extensions import mypyc_attr, trait + +@mypyc_attr(free_list_len=1) +class UsesFreeList: + pass + +@mypyc_attr(free_list_len=None) +class NoFreeList: + pass + +@mypyc_attr(free_list_len=2) # E: Unsupported value for "free_list_len": 2 +class FreeListError: + pass + +@trait +@mypyc_attr(free_list_len=1) # E: "free_list_len" can't be used with traits +class NonNative: + pass diff --git a/mypyc/test-data/run-classes.test b/mypyc/test-data/run-classes.test index b25dc9458fd16..d2e970415f5e4 100644 --- a/mypyc/test-data/run-classes.test +++ b/mypyc/test-data/run-classes.test @@ -3794,3 +3794,37 @@ assert t.native == 43 assert t.generic == "{}" assert t.bitfield == 0x0C assert t.default == 10 + +[case testPerTypeFreeList] +from mypy_extensions import mypyc_attr + +a = [] + +@mypyc_attr(free_list=1) +class Foo: + def __init__(self, x: int) -> None: + self.x = x + a.append(x) + +def test_alloc() -> None: + x: Foo | None + y: Foo | None + + x = Foo(1) + assert x.x == 1 + x = None + + x = Foo(2) + assert x.x == 2 + y = Foo(3) + assert x.x == 2 + assert y.x == 3 + x = None + y = None + assert a == [1, 2, 3] + + x = Foo(4) + assert x.x == 4 + y = Foo(5) + assert x.x == 4 + assert y.x == 5 From 24199fec21dd6f95e88aecba0434f18fcc654164 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 2 Sep 2025 10:00:53 +0000 Subject: [PATCH 2/3] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypyc/irbuild/prepare.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/mypyc/irbuild/prepare.py b/mypyc/irbuild/prepare.py index 008d78f6e9a05..f47dcb52ceb70 100644 --- a/mypyc/irbuild/prepare.py +++ b/mypyc/irbuild/prepare.py @@ -366,11 +366,7 @@ def prepare_class_def( if free_list_len == 1: ir.reuse_freed_instance = True else: - errors.error( - f'Unsupported value for "free_list_len": {free_list_len}', - path, - line, - ) + errors.error(f'Unsupported value for "free_list_len": {free_list_len}', path, line) # Check for subclassing from builtin types for cls in info.mro: From 18bc7c601ab64e34706c9bb4c180193aa7081b71 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 2 Sep 2025 11:44:10 +0100 Subject: [PATCH 3/3] Fix test on 3.9 --- mypyc/test-data/run-classes.test | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mypyc/test-data/run-classes.test b/mypyc/test-data/run-classes.test index d2e970415f5e4..79ad2fa0a03b7 100644 --- a/mypyc/test-data/run-classes.test +++ b/mypyc/test-data/run-classes.test @@ -3796,6 +3796,8 @@ assert t.bitfield == 0x0C assert t.default == 10 [case testPerTypeFreeList] +from __future__ import annotations + from mypy_extensions import mypyc_attr a = []