-
-
Notifications
You must be signed in to change notification settings - Fork 31.3k
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
bpo-39102: Increase Enum performance up to 10x times (3x average) #17669
Conversation
# Conflicts: # Lib/enum.py
Hello, and thanks for your contribution! I'm a bot set up to make sure that the project can legally accept this contribution by verifying everyone involved has signed the PSF contributor agreement (CLA). CLA MissingOur records indicate the following people have not signed the CLA: For legal reasons we need all the people listed to sign the CLA before we can look at your contribution. Please follow the steps outlined in the CPython devguide to rectify this issue. If you have recently signed the CLA, please wait at least one business day You can check yourself to see if the CLA has been received. Thanks again for the contribution, we look forward to reviewing it! |
super().__setitem__(key, value) | ||
|
||
@property |
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.
the deprecation part could be addressed in a separate commit.
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 don't quite understand how it should look like, though. Should I change anything now?
Just realized that _value_ and _name_ attrs remained independent from name and value and could be different. This commit fixes it.
Run builtin python tests directly from python test module Refactor patch applying
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.
Sorry a proper review is taking so long -- there is a lot to go over here.
What I can say so far:
name
andvalue
must show up in a member'sdir()
;_leading_underscore_names
andplain_names
must not be used inEnumMeta
norEnum
as then the user cannot use those same names -- that's why_sunder_
and__dunder__
names are used instead;
I do have some private code that I haven't merged in yet that does away with DynamicClassAttribute
-- I'll try to get that done in the next few weeks. I think a lot of the work you have done here will still be applicable.
Thank you for your efforts!
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.
Feedback needed
value = self._value_ | ||
if self.name is not None: | ||
return f're.{self.name}' | ||
value = self.value |
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.
Should use _repr_
cache here
for k, v in c.__dict__.items() | ||
if isinstance(v, DynamicClassAttribute)} | ||
|
||
# Reverse value->name map for hashable values. | ||
enum_class._value2member_map_ = {} | ||
|
||
# used to speedup __str__, __repr__ and __invert__ calls when applicable |
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.
Despite the fact that this works pretty well and gives a great performance boost, I'm not sure that it was implemented right, so feedback is highly appreciable.
# TODO: Maybe remove try/except block and setting __context__ in this case? | ||
try: | ||
exc = None | ||
result = cls._missing_(value) | ||
except Exception as e: | ||
exc = e | ||
result = None | ||
# Huge boost for standard enum | ||
if cls._missing_ is Enum._missing_: | ||
raise | ||
else: | ||
e.__context__ = ValueError(f'{value!r} is not a valid {cls.__qualname__}') | ||
raise |
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.
With this try/except block code runs twice slower for non-enum values checks, then without it .
Do we really need to bother checking all errors and add ValueError
to context , knowing it will happen either in standard Enum._missing_
or in user-defined code? In fact, Flag._missing_
also raises same error as Enum._missing_
and handled and rerised too.
@ethanfurman, any chance this PR will be reviewed in the near future? |
Not a core team member, but I would really love this performance boost - any chance this will be reviewed soon? |
I have some other changes planned, which include some speed ups. We'll see the changes in 3.10 (3.9 is in bug-fix only prior to first release). |
@TylerYep, you can check out looks like I haven’t uploaded it on PyPi though. Maybe I’ll upload it on this weekend, but for now it’s still should be installable via pip: pip install git+https://github.com/MrMrRobat/fastenum.git |
This PR takes the approach of caching things, perhaps that is best. However, when I looked at the Flag code several months ago (being unaware of this PR), I kept wondering why carry such a heavy and complicated Proof of concept is here: belm0#1 However, this refactor is thwarted by some odd edge behavior of
|
@belm0 Thanks for the concise summary. 1 is a bug, and needs to be changed. |
It was a design choice, wasn't it? From my understanding, the primary motivation of |
@belm0 If a concrete data type is mixed into an The primary motivation behind What would be most helpful at this point is some actual code samples of "magic" integer flags that have inversion applied to them, and looking at how the surrounding code is treating the result of that operation. Hmmm.... - just thinking out loud here, but it seems to me that the problem is that integer flags are (usually? always?) unsigned, but Python doesn't have that particular concept. Perhaps the thing to do is to make Thoughts? |
I don't agree with these rationales-- it probably won't be productive for us to argue it now.
The only reasonable way to do flag negation with signed integers is to negate and then mask with the valid bits. This is effectively what Flag does, and it's what IntFlag should do as well. Either no one is using No one wants or is expecting a flag/enum component to have multiple representations ( |
@belm0 : As I alluded to earlier, As you say, it is the only reasonable way to use negated flags. I'm more comfortable making that change now, thank you for your feedback. |
What is the use case for this one? Given something like {B=3, C=4}, what will be the defined behavior of the new iteration added to 3.10? |
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.
For 3.10 and supporting __init_subclass__
I've made several structural changes to the enum
implementation. In doing that I included many of the optimizations that you spoke of. If you are not already in the ACK file I'll add you there in my next commit.
Improvements made since your PR:
enum.property
, a subclass ofDynamicClassAttribute
, is used : it avoids the slow__getattr__
lookupdecompose
has been removed (with belm0's help)_invert_
is cached
Things that likely will not change:
_name_
as anenum.property
_value_
as anenum.property
__member_map__
as the public API for accessing enum internals
Things I am open to:
- using
set()
instead of lists - using internal
dict()
s instead of matchinglist()
s to track names/values - using
f-strings
- caching
__member_map__
instead of creating a new one on each access
If you would like to make another PR based on current 3.10 code based on above comments I would be happy to review and commit it. If you don't have time or the desire to tackle this again, you still have my thanks for the ideas and your help in the improvements that have made it in so far.
A Python core developer has requested some changes be made to your pull request before we can consider merging it. If you could please address their requests along with any other requests in other reviews from core developers that would be appreciated. Once you have made the requested changes, please leave a comment on this pull request containing the phrase |
@MrMrRobat would you mind running your enum benchmark against head? https://gist.github.com/MrMrRobat/94b5ca3e4098669d93a6127e70359307 |
Creating of enums got slower with the extra dance I had to do to support |
@ethanfurman, thanks for the feedback! Should I open another PR, or will it be enough to just merge
I don't quite understand what member map you are referring to, could you please clarify? |
@belm0, sure, I will. |
@MrMrRobat Sorry, I meant |
Increase Enum performance
https://bugs.python.org/issue39102
Partial benchmark result, to see more, check full benchmark result and source code
Full benchmark result and source code
Changes
Enum.__new__
EnumMeta.__getattr__
,Enum.name
and.value
in members__dict__
for faster accessEnum._name_
and._value_
, should use public.name
and.value
nowEnum._member_names_
with._unique_member_map_
for faster lookups and iteration_EmumMeta._member_names
and._last_values
with.members
mapping (deprecation warning on using old attrs)_str_
,_repr_
and_invert_
attrs to cache results of functions of the same dunder names (x35 speed boost) (948d3de)DynamicClassAttribute
without need to use slow__getattr__
https://bugs.python.org/issue39102