Skip to content

Commit e35e05f

Browse files
authored
[mypyc] Allow defining a single-item free "list" for a native class (#19785)
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.
1 parent b8ee1f5 commit e35e05f

File tree

4 files changed

+74
-3
lines changed

4 files changed

+74
-3
lines changed

mypyc/irbuild/prepare.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -351,13 +351,23 @@ def prepare_class_def(
351351
ir = mapper.type_to_ir[cdef.info]
352352
info = cdef.info
353353

354-
attrs = get_mypyc_attrs(cdef)
354+
attrs, attrs_lines = get_mypyc_attrs(cdef)
355355
if attrs.get("allow_interpreted_subclasses") is True:
356356
ir.allow_interpreted_subclasses = True
357357
if attrs.get("serializable") is True:
358358
# Supports copy.copy and pickle (including subclasses)
359359
ir._serializable = True
360360

361+
free_list_len = attrs.get("free_list_len")
362+
if free_list_len is not None:
363+
line = attrs_lines["free_list_len"]
364+
if ir.is_trait:
365+
errors.error('"free_list_len" can\'t be used with traits', path, line)
366+
if free_list_len == 1:
367+
ir.reuse_freed_instance = True
368+
else:
369+
errors.error(f'Unsupported value for "free_list_len": {free_list_len}', path, line)
370+
361371
# Check for subclassing from builtin types
362372
for cls in info.mro:
363373
# Special case exceptions and dicts

mypyc/irbuild/util.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ def get_mypyc_attr_literal(e: Expression) -> Any:
9696
return False
9797
elif isinstance(e, RefExpr) and e.fullname == "builtins.None":
9898
return None
99+
elif isinstance(e, IntExpr):
100+
return e.value
99101
return NotImplemented
100102

101103

@@ -110,20 +112,23 @@ def get_mypyc_attr_call(d: Expression) -> CallExpr | None:
110112
return None
111113

112114

113-
def get_mypyc_attrs(stmt: ClassDef | Decorator) -> dict[str, Any]:
115+
def get_mypyc_attrs(stmt: ClassDef | Decorator) -> tuple[dict[str, Any], dict[str, int]]:
114116
"""Collect all the mypyc_attr attributes on a class definition or a function."""
115117
attrs: dict[str, Any] = {}
118+
lines: dict[str, int] = {}
116119
for dec in stmt.decorators:
117120
d = get_mypyc_attr_call(dec)
118121
if d:
119122
for name, arg in zip(d.arg_names, d.args):
120123
if name is None:
121124
if isinstance(arg, StrExpr):
122125
attrs[arg.value] = True
126+
lines[arg.value] = d.line
123127
else:
124128
attrs[name] = get_mypyc_attr_literal(arg)
129+
lines[name] = d.line
125130

126-
return attrs
131+
return attrs, lines
127132

128133

129134
def is_extension_class(path: str, cdef: ClassDef, errors: Errors) -> bool:

mypyc/test-data/irbuild-classes.test

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1734,3 +1734,23 @@ class NonNative:
17341734
class InheritsPython(dict):
17351735
def __new__(cls) -> InheritsPython:
17361736
return super().__new__(cls) # E: super().__new__() not supported for classes inheriting from non-native classes
1737+
1738+
[case testClassWithFreeList]
1739+
from mypy_extensions import mypyc_attr, trait
1740+
1741+
@mypyc_attr(free_list_len=1)
1742+
class UsesFreeList:
1743+
pass
1744+
1745+
@mypyc_attr(free_list_len=None)
1746+
class NoFreeList:
1747+
pass
1748+
1749+
@mypyc_attr(free_list_len=2) # E: Unsupported value for "free_list_len": 2
1750+
class FreeListError:
1751+
pass
1752+
1753+
@trait
1754+
@mypyc_attr(free_list_len=1) # E: "free_list_len" can't be used with traits
1755+
class NonNative:
1756+
pass

mypyc/test-data/run-classes.test

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3794,3 +3794,39 @@ assert t.native == 43
37943794
assert t.generic == "{}"
37953795
assert t.bitfield == 0x0C
37963796
assert t.default == 10
3797+
3798+
[case testPerTypeFreeList]
3799+
from __future__ import annotations
3800+
3801+
from mypy_extensions import mypyc_attr
3802+
3803+
a = []
3804+
3805+
@mypyc_attr(free_list=1)
3806+
class Foo:
3807+
def __init__(self, x: int) -> None:
3808+
self.x = x
3809+
a.append(x)
3810+
3811+
def test_alloc() -> None:
3812+
x: Foo | None
3813+
y: Foo | None
3814+
3815+
x = Foo(1)
3816+
assert x.x == 1
3817+
x = None
3818+
3819+
x = Foo(2)
3820+
assert x.x == 2
3821+
y = Foo(3)
3822+
assert x.x == 2
3823+
assert y.x == 3
3824+
x = None
3825+
y = None
3826+
assert a == [1, 2, 3]
3827+
3828+
x = Foo(4)
3829+
assert x.x == 4
3830+
y = Foo(5)
3831+
assert x.x == 4
3832+
assert y.x == 5

0 commit comments

Comments
 (0)