Skip to content
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

Optimize the OptionKey class more #14248

Open
wants to merge 6 commits into
base: master
Choose a base branch
from

Conversation

dcbaker
Copy link
Member

@dcbaker dcbaker commented Feb 12, 2025

These changes attempt to make the OptionKey faster to initializer and to compare.

with some light testing this makes initializing options keys roughly 5x as fast (when initializing the same key many times) and comparing two pre-initialized keys roughly twice as fast.

It's become a performance issue.
@dcbaker dcbaker force-pushed the submit/optionkey-performance branch from 7c914ca to 5778847 Compare February 12, 2025 19:40
@jpakkane
Copy link
Member

This will conflict with absolutely everything in the optionrefactor branch. :(

@dcbaker
Copy link
Member Author

dcbaker commented Feb 12, 2025

I have zero idea why all of the test are failing in CI. They all pass 100% locally

mesonbuild/options.py Outdated Show resolved Hide resolved
@@ -106,12 +106,13 @@ class OptionKey:
internally easier to reason about and produce.
"""

__slots__ = ['name', 'subproject', 'machine', '_hash']
__slots__ = ['name', 'subproject', 'machine', '_hash', '_as_tuple']
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm really not sure about this one. Tuple representation is only used for comparison, and I doubt we really need to sort OptionKeys. Especially since we now use the hash for equality comparison.

@@ -192,6 +193,7 @@ def __repr__(self) -> str:
return f'OptionKey({self.name!r}, {self.subproject!r}, {self.machine!r})'

@classmethod
@lru_cache(None)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this has a big impact. It is probably only used for parsing options given by the command line.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This gets used in a surprising number of places, many of them because we still are doing user input validation below the interpreter.

@dcbaker dcbaker force-pushed the submit/optionkey-performance branch 2 times, most recently from c13d2c0 to c7ec44e Compare February 12, 2025 20:44
@bonzini
Copy link
Collaborator

bonzini commented Feb 12, 2025

Shouldn't the hash only be used as a quick filter, only to then test the tuple if the hash matches?

@QuLogic
Copy link
Member

QuLogic commented Feb 13, 2025

s/Optimze/Optimize/ in the PR title.

@dcbaker dcbaker changed the title Optimze the OptionKey class more Optimize the OptionKey class more Feb 13, 2025
@dcbaker
Copy link
Member Author

dcbaker commented Feb 13, 2025

Shouldn't the hash only be used as a quick filter, only to then test the tuple if the hash matches?

If the hashes match and they're not equal we have a problem, since that would mean that OptionKey('a') in {OptionKey('a'): ...} would have different behavior than OptionKey('a') == OptionKey('a'), which would seem like a bug.

@bonzini
Copy link
Collaborator

bonzini commented Feb 13, 2025

No, the hash collections always check with __eq__, hashing is only used to start the lookup at a good place.

If you create a subclass of str which always returns 1 for hash, set and dict performance will certainly suck but they will give correct results.

@dcbaker dcbaker force-pushed the submit/optionkey-performance branch from c7ec44e to e43f21c Compare February 13, 2025 02:19
@dcbaker
Copy link
Member Author

dcbaker commented Feb 13, 2025

Okay, in that case the commit is bogus and we should just go back to the tuple == tuple thing

Using lazy_property would be great, but since the OptionKey is immutable
it doesn't work, so we'll just calculate it at initialization time.
Because of all the caching that should be relatively fast.
Since OptionKeys are immutable, it's fine to cache this.
This doesn't use lru_cache because we can re-use the hash calculation in
a cache miss by not doing so, which makes a cache miss faster.
It's now intended that the `factory()` method will be used when not
using `from_string`, so there's no reason to make it convenient to use
the main initializer, and passing the hash in is convenient for the
factory method.
@dcbaker dcbaker force-pushed the submit/optionkey-performance branch from e43f21c to b4b1f81 Compare February 13, 2025 02:22
# in a cache miss situation.
_hash = hash((name, subproject, machine))
try:
return _OPTIONS_CACHE[_hash]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is unlikely, but two objects can have the same hash but not be equal. I'm not sure that the cache lookup based on the hash only is a good idea. If this is done so many times that computing the hash becomes the bottleneck, maybe it is better to simply cache the has.

@@ -192,6 +195,25 @@ def __str__(self) -> str:
def __repr__(self) -> str:
return f'OptionKey({self.name!r}, {self.subproject!r}, {self.machine!r})'

@classmethod
def factory(cls, name: str, subproject: str = '', machine: MachineChoice = MachineChoice.HOST) -> OptionKey:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't tried to implement it, but I think you can obtain the same effect without forcing the users to call the .factory() class method by implementing __new__() instead.

# the _type option to the constructor is kinda private. We want to be
# able to save the state and avoid the lookup function when
# pickling/unpickling, but we need to be able to calculate it when
# constructing a new OptionKey
object.__setattr__(self, 'name', name)
object.__setattr__(self, 'subproject', subproject)
object.__setattr__(self, 'machine', machine)
object.__setattr__(self, '_hash', hash((name, subproject, machine)))
object.__setattr__(self, '_hash', hash_ if hash_ is not None else hash((name, subproject, machine)))
object.__setattr__(self, '_as_tuple', (self.subproject, self.machine, self.name))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This starts to look very very very similar to a named tuple 🙂

@bruchar1
Copy link
Member

@bonzini is right about hash collisions. Sorry for guiding you in the wrong direction...

Like @dnicolodi said, it is possible to implement the cache without breaking the API, using the __new__ method. Here is my try in this: #14250.

@bonzini bonzini added this to the 1.8 milestone Feb 13, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants