-
-
Notifications
You must be signed in to change notification settings - Fork 30.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
gh-103193: cache calls to inspect._shadowed_dict
in inspect.getattr_static
#104267
Conversation
The
So I don't think this is safe. |
Well, good that we're discovering holes in What about something like this, where # add @lru_cache() to _shadowed_dict, like in this PR currently
def getattr_static(obj, attr, default=_sentinel):
try:
# existing getattr_static implementation here
finally:
_shadowed_dict.cache_clear() |
How does the performance comparison look if you make I think if you did that, then it would be safe to put an LRU cache on I think it should be a bounded cache, though, not unbounded. The pathological scenario would be someone creating lots of transient classes inside a function, and calling |
>>> functools.lru_cache(lambda: 1).cache_info()
CacheInfo(hits=0, misses=0, maxsize=128, currsize=0) |
It's not as fast as this PR, but it's a lot faster than |
def _shadowed_dict(klass): | ||
return _shadowed_dict_from_mro_tuple(_static_getmro(klass)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I considered inlining the calls to _static_getmro
, but I thought this made it more readable. It also preserves git blame
better, and having to pass through this function doesn't seem to have much impact on performance
Here's the new performance numbers using the
The same benchmark on
With this PR now, On |
(Skipping news because, if this patch is correct, it shouldn't change behaviour at all other than improving performance. And the news entry in https://github.com/python/cpython/pull/103195/files should already cover the performance boost.) |
inspect._shadowed_dict
in inspect.getattr_static
Thanks @carljm :D I listed you as a co-author on the commit; I don't think I would have thought of using the mro tuple as a cache key on my own :) |
…getattr_static` (python#104267) Co-authored-by: Carl Meyer <carl@oddbird.net>
EDIT: These performance numbers are out of date with the current PR; read further down the thread for the up-to-date numbers.
This dramatically speeds up calls to
inspect.getattr_static
.Here are benchmark results on
main
, using @sobolevn's benchmark script from #103193 (comment):And here are results with this PR:
With this PR,
inspect.getattr_static
is fast enough that evenisinstance()
calls like this, with "pathological" runtime-checkable protocols, are faster than they were on Python 3.11:Pathological protocol
This approach makes me a little nervous. There are two plausible reasons I thought of why adding a cache here might not be a good idea:
klass
argument to be held by the cache even after they've been deleted elsewhere, which could be unexpected behaviour for a low-level function likegetattr_static
__dict__
attribute is shadowed could change at some point during the lifetime of a class.However, I think we're okay on both points.
For objection (1):
_shadowed_dict
is only ever called on type objects. The vast majority of classes are defined once in the global namespace and never deleted, so it shouldn't be an issue that the cache is holding strong references to the type objects. (If we do think this is an issue, I also experimented with a version of this PR that uses aweakref.WeakKeyDictionary
as a cache. It also sped things up, but not by nearly as much.)For objection (2): It doesn't seem to be possible, once a class has been created, to change the class's
__dict__
attribute, at least from Python code. So for any given classklass
,_shadowed_dict(klass)
should always return the same result.@carljm, what do you think?
inspect.getattr_static
#103193