[mypyc] Add memoization support for functools.cached_property#21611
[mypyc] Add memoization support for functools.cached_property#21611chadrik wants to merge 1 commit into
Conversation
…ve classes
Previously mypyc silently compiled functools.cached_property as a plain
non-caching property: the semantic analyzer marks it is_property /
is_settable_property like builtins.property, and mypyc had no handling of
its own, so the decorated body re-ran on every access -- a severe silent
performance regression.
Where the cached value is stored depends on whether the instance has a
__dict__:
- A native (compiled) class has no instance __dict__. Its cached_property is
backed by a hidden native slot instead, so caching works without a __dict__.
- An interpreted subclass of a native class is given its own instance __dict__
by CPython (a heap subclass of an extension type that lacks one has a
__dict__ added automatically). A cached_property defined on the interpreted
subclass therefore memoizes into that __dict__ with no special handling,
while an inherited native cached_property keeps using the native slot. No
manual __dict__ setup (e.g. assigning self.__dict__ = {}) is required.
- The only case that cannot cache is a class that suppresses its __dict__ with
__slots__: a functools.cached_property defined on it raises at access time.
This is ordinary CPython behaviour, unchanged by mypyc.
25dc3fa to
ece6088
Compare
|
I'm ready for feedback! |
p-sawicki
left a comment
There was a problem hiding this comment.
apologies for the delay on the review.
the implementation looks good, couple notes about tests.
| try: | ||
| # There is no cached value to delete now | ||
| del c.unboxed | ||
| except AttributeError: | ||
| pass | ||
| else: | ||
| assert False |
There was a problem hiding this comment.
we usually use assertRaises from fixtures/testutil.py for checks like this.
| def del_unboxed(c): | ||
| del c.unboxed | ||
|
|
||
| [case testCachedPropertyMultiModule] |
There was a problem hiding this comment.
maybe move this test to run-multimodule.test to also automatically get a test case that compiles the files in separate mode with each module creating a different C library.
| try: | ||
| Slotted().slot_val | ||
| assert False, "expected TypeError" | ||
| except TypeError as e: | ||
| assert "__dict__" in str(e) |
There was a problem hiding this comment.
also could use assertRaises here.
| @cached_property | ||
| def boxed(self) -> List[int]: | ||
| self.compute_count += 1 | ||
| return [self.x] |
There was a problem hiding this comment.
would be good to add a test with a more complicated function body: multiple return statements and branches to make sure the transform can handle them correctly.
Previously mypyc silently compiled
functools.cached_propertyas a plain non-caching property which resulted in massive slowdowns on a real world project. The semantic analyzer markscached_propertyasis_property/is_settable_propertylikebuiltins.property, and mypyc had no handling of its own, so the decorated body re-ran on every access.Implementation:
__mypyc_cache__<name>) on the ClassIR. Unboxed property types use a boxed slot soNULLuniformly represents 'not yet cached'. A subclass redefining an inherited cached property reuses the base slot (mirroring how functools shares the instance__dict__entry).functools.cached_property; deletion clears the slot and raisesAttributeErrorwhen nothing is cached, matching CPython semantics.builtins.propertyas well).