GeezerVison is showing github.com — gh-85160: improve performance of singledispatchmethod by eendebakpt · Pull Request #106448 · python/cpython
Conversation
eendebakpt
commented
Jul 5, 2023
•
edited
Loading
Uh oh!
There was an error while loading. Please reload this page.
This PR implements the idea from #85160 to improve performance of the singledispatchmethod by caching the generated dispatch method.
(updated) benchmark script:
import timeit
class Test:
@singledispatchmethod
def normal(self, item, arg):
print('general')
@normal.register
def _(self, item:int, arg):
return item + arg
@singledispatchmethod
@staticmethod
def static(item, arg):
print('general')
@static.register
@staticmethod
def _(item:int, arg):
return item + arg
class SlotClass():
__slots__ = ("a", "b")
def __init__(self):
self.a = "zero"
self.b = "one"
@singledispatchmethod
def normal(self, item, arg):
print('general')
@normal.register
def _(self, item:int, arg):
return item + arg
print('normal ', timeit.timeit('t.normal(1, 1)', globals={'t': Test()}))
print('static method', timeit.timeit('Test.static(1, 1)', globals={'t': Test(), 'Test': Test}))
print('static method instance', timeit.timeit('t.static(1, 1)', globals={'t': Test()}))
print('slotted class ', timeit.timeit('s.normal(1, 1)', globals={'s': SlotClass()}))
Output on main:
normal 2.5062581849997514
static method 2.7215049250007723
static method instance 2.922215309999956
slotted class 2.8028633869998885
This PR:
normal 0.9856528150003214
static method 0.9481567359998735
static method instance 0.9450106439999217
slotted class 0.9822057020001012
PR #23213 (alternative implementation)
normal 0.79438615700019
static method 3.4307698050006366
static method instance 0.7644647329998406
slotted class 3.4285360860003493
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
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.
Thanks! It looks like this doesn't differ significantly from #23213, which has already been open for a while -- is that correct?
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Thanks! It looks like this doesn't differ significantly from #23213, which has already been open for a while -- is that correct?
You are right, I somehow missed that one. I will compare the two PRs. If this one does not have any benefits I will close this one.
|
AlexWaygood reacted with thumbs up emoji
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Thanks! It looks like this doesn't differ significantly from #23213, which has already been open for a while -- is that correct?
@AlexWaygood The two implementations are similar, but there are some differences so perhaps one is preferable over the other.
|
AlexWaygood reacted with thumbs up emoji
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
|
Looking at the different implementation performance, if it does not become too complex,
maybe _dispatch_method = None could be set on the singledispatchmethod class, then __get__ tries to assign __dict__ and fallsback on the _dispatch_method logic? May not be worth the extra complexity though
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
|
@eendebakpt Well, I will take a look at this PR by this weekend.
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
AlexWaygood
commented
Jul 21, 2023
•
edited
Loading
Uh oh!
There was an error while loading. Please reload this page.
|
(I haven't had time yet, but I'd also like to take a look! I'll try to do so this weekend :)
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
AlexWaygood
left a comment
•
edited
Loading
Uh oh!
There was an error while loading. Please reload this page.
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'm a little concerned that this would cache singledispatchmethods across different instances of the same class. That would mean that singledispatchmethods would have pretty different semantics to most other methods in Python.
Here's what you get with most methods in Python:
>>> class Klass:
... def method(self): pass
...
>>> Klass().method is Klass().method
False
And here's the behaviour on main with respect to singledispatchmethod:
>>> from functools import singledispatchmethod
>>> class Foo:
... @singledispatchmethod
... def neg(self, arg): raise NotImplementedError
...
>>> Foo().neg is Foo().neg
False
>>> Foo().neg.bar = 'bar'
>>> Foo().neg.bar
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'function' object has no attribute 'bar'
Here's the behaviour you get with this PR branch:
>>> from functools import singledispatchmethod
>>> class Foo:
... @singledispatchmethod
... def neg(self, arg): raise NotImplementedError
...
>>> Foo().neg is Foo().neg
True
>>> Foo().neg.bar = 'bar'
>>> Foo().neg.bar
'bar'
Does this matter? Not sure. But in general, I feel like it's unfortunate to have the behaviour of singledispatchmethods differ from "normal" methods, and I'd imagine that's why this class was originally written the way that it is
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
corona10 reacted with thumbs up emoji
|
Note that I don't see the same behaviour change with #23213 (as it's using a per-instance cache, rather than caching the same singledispatchmethod for all instances of Foo), so I generally prefer that approach. (But I like that you added a test for this PR, and the author of that PR hasn't responded to your review suggestion from a few weeks ago.)
|
corona10 reacted with thumbs up emoji
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Note that I don't see the same behaviour change with #23213 (as it's using a per-instance cache, rather than caching the same singledispatchmethod for all instances of Foo), so I generally prefer that approach. (But I like that you added a test for this PR, and the author of that PR hasn't responded to your review suggestion from a few weeks ago.)
If we decide the approach from this PR is not the right one (for the reasons mentioned above) and there is no response from the other author I can modify this PR and add another test for the behavior change.
The behavior from current main also differs a bit from "normal" methods, for example
from functools import singledispatch
class Foo:
def bar(self):
pass
@singledispatchmethod
def neg(self, arg): raise NotImplementedError
f = Foo()
print(f.bar is f.bar) # prints False
print(f.bar == f.bar) # prints True
print(f.neg is f.neg) # prints False on main, on this PR that changes to True
print(f.neg == f.neg) # prints False on main, which differs from the "normal" method bar
print(f.neg.register == f.neg.register) # prints True
F=Foo()
f.neg.bar=1 # works as expected
f.neg.bar # raises an error! that is really strange. for both PRs one can set f.neg.bar
In a certain sense with this PR the behavior is more like normal methods than current main, since we can now set f.neg.bar=1 and use than later. But the behaviour might be misleading since it is not per instance but per class.
A per object cache (like #85160) definitely feels like a safer option. I quickly checked whether a lru_cache on the __get__ might be an option to improve performance. The performance is good, but I think the behavior will be even stranger.
|
AlexWaygood reacted with thumbs up emoji
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
corona10
commented
Jul 22, 2023
•
edited
Loading
Uh oh!
There was an error while loading. Please reload this page.
|
@eendebakpt
I have the same concern with @AlexWaygood
(Thank you Alex, for catching the side-effect, I was finding unexpected side-effects, and you pointed it out)
Well, IMO, the different result of the is operator doesn't look big issue for me. It is just for implementation detail.
(I think that is same as we don't recommend to compare int with is operator)
But assigning behavior is a big concern because this is quite a common operation and the user can meet unexpected behavior that the user didn't meet at older versions.
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
AlexWaygood and eendebakpt reacted with thumbs up emoji
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Reviewers
corona10
Awaiting requested review from corona10
+1 more reviewer
CaselIT
CaselIT left review comments
Reviewers whose approvals may not affect merge requirements
Uh oh!
There was an error while loading. Please reload this page.
Milestone
No milestone
Development
Successfully merging this pull request may close these issues.
Uh oh!
There was an error while loading. Please reload this page.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
|