GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — gh-85160: improve performance of singledispatchmethod by eendebakpt · Pull Request #106448 · python/cpython

Conversation

@eendebakpt

@eendebakpt eendebakpt commented Jul 5, 2023 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Contributor

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
  • Issue: singledispatchmethod significantly slower than singledispatch #85160

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@eendebakpt
generate dispatch method only once
5277335
@eendebakpt
eendebakpt requested a review from rhettinger as a code owner July 5, 2023 09:28
@bedevere-bot bedevere-bot added the awaiting review label Jul 5, 2023
@bedevere-bot bedevere-bot mentioned this pull request Jul 5, 2023
singledispatchmethod significantly slower than singledispatch #85160
Closed
eendebakpt and others added 2 commits July 5, 2023 13:27
@eendebakpt
fix whitespace
5eb3d3b
@blurb-it
📜🤖 Added by blurb_it.
ad5bd22
AlexWaygood
AlexWaygood reviewed Jul 5, 2023
View reviewed changes

@AlexWaygood AlexWaygood left a comment

Copy link
Copy Markdown
Member

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.

All reactions
@eendebakpt

eendebakpt commented Jul 5, 2023

Copy link
Copy Markdown
Contributor Author

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
All reactions
  • 👍 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

eendebakpt added 2 commits July 5, 2023 20:20
@eendebakpt
cache dispath method on singledispatch instance
abd1628
@eendebakpt
add test for slotted classes
4e068d4
@eendebakpt eendebakpt mentioned this pull request Jul 5, 2023
gh-85160: Optimized singledispatchmethod access (noticeable improvement). #23213
Closed
@eendebakpt

eendebakpt commented Jul 5, 2023

Copy link
Copy Markdown
Contributor Author

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
All reactions
  • 👍 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

CaselIT
CaselIT reviewed Jul 5, 2023
View reviewed changes
Comment thread Lib/functools.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

@eendebakpt
add missing statement
802f3d2
@CaselIT

CaselIT commented Jul 5, 2023

Copy link
Copy Markdown

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

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@rhettinger
rhettinger removed their request for review July 7, 2023 09:07
corona10
corona10 reviewed Jul 8, 2023
View reviewed changes
Comment thread Lib/functools.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

corona10
corona10 reviewed Jul 8, 2023
View reviewed changes
Comment thread Misc/NEWS.d/next/Library/2023-07-05-11-28-57.gh-issue-85160.t925jm.rst Resolved

Uh oh!

There was an error while loading. Please reload this page.

@eendebakpt @corona10
Apply suggestions from code review
72c8b0f
Co-authored-by: Dong-hee Na <[email protected]>
@eendebakpt
eendebakpt requested a review from corona10 July 20, 2023 09:59
@corona10

corona10 commented Jul 21, 2023

Copy link
Copy Markdown
Member

@eendebakpt Well, I will take a look at this PR by this weekend.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@corona10 corona10 self-assigned this Jul 21, 2023
@AlexWaygood

AlexWaygood commented Jul 21, 2023 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Member

(I haven't had time yet, but I'd also like to take a look! I'll try to do so this weekend :)

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@AlexWaygood
AlexWaygood self-requested a review July 21, 2023 15:01
AlexWaygood
AlexWaygood reviewed Jul 21, 2023
View reviewed changes

@AlexWaygood AlexWaygood left a comment •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Member

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
All reactions
  • 👍 1 reaction
@AlexWaygood

AlexWaygood commented Jul 21, 2023

Copy link
Copy Markdown
Member

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
All reactions
  • 👍 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@eendebakpt

eendebakpt commented Jul 21, 2023

Copy link
Copy Markdown
Contributor Author

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
All reactions
  • 👍 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@corona10

corona10 commented Jul 22, 2023 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Member

@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.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@corona10

corona10 commented Jul 22, 2023

Copy link
Copy Markdown
Member

@eendebakpt
Would you like to re-submit the patch based on #23213 with a co-authored commit of the origin author?
cc @AlexWaygood

AlexWaygood and eendebakpt reacted with thumbs up emoji
All reactions
  • 👍 2 reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@eendebakpt eendebakpt mentioned this pull request Jul 23, 2023
gh-85160: improve performance of singledispatchmethod #107148
Merged
@eendebakpt

eendebakpt commented Jul 23, 2023

Copy link
Copy Markdown
Contributor Author

Closing in favor of #107148

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@eendebakpt eendebakpt closed this Jul 23, 2023
@eendebakpt
eendebakpt deleted the singledispatchmethod branch June 26, 2025 13:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

@AlexWaygood AlexWaygood AlexWaygood left review comments
@corona10 corona10 Awaiting requested review from corona10
+1 more reviewer
@CaselIT 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.

Assignees

@corona10 corona10

Labels

awaiting review

Projects

None yet

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.

5 participants

@eendebakpt @CaselIT @corona10 @AlexWaygood @bedevere-bot
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.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.