gh-153198 Optimize set and frozenset membership lookup#153199
gh-153198 Optimize set and frozenset membership lookup#153199rajat315315 wants to merge 5 commits into
Conversation
…t-path for identical Unicode keys
|
Most changes to Python require a NEWS entry. Add one using the blurb_it web app or the blurb command-line tool. If this change has little impact on Python users, wait for a maintainer to apply the |
StanFromIreland
left a comment
There was a problem hiding this comment.
Please provide benchmarks, run on a PGO + LTO build.
| #define PERTURB_SHIFT 5 | ||
|
|
||
| static int | ||
| static inline Py_ALWAYS_INLINE int |
There was a problem hiding this comment.
Is the compiler not already specialising this?
There was a problem hiding this comment.
Below is a response from an AI agent:
Without explicitly using Py_ALWAYS_INLINE, the compiler is highly unlikely to inline or specialize this automatically for a few key reasons:
1. The Heuristics Threshold (Function Size)
Modern C compilers (GCC, Clang, MSVC) use conservative heuristics to decide what to inline. They estimate the number of instructions in a function, and if it exceeds a certain threshold, they won't inline it to avoid bloating the binary.
Because set_do_lookup contains:
- An outer loop (
while (1)) - A nested loop (
do { ... } while) - Complex index wrapping & perturbation logic (
perturb >>= PERTURB_SHIFT) - Multiple status checks & returns
It is too large for compilers to automatically inline under default heuristics.
2. Compilers Cannot Devirtualize Pointers in Standalone Functions
If a function is not inlined, it must be compiled as a separate, general-purpose function. In that compiled code:
status = compare_entry(so, table, entry, key, hash);- must remain a generic function pointer call (an indirect branch).
- The compiler cannot inline the comparison logic because it doesn't know which function is being called at runtime.
3. Even with LTO + PGO, Auto-Specialization is Not Guaranteed
While Link-Time Optimization (LTO) and Profile-Guided Optimization (PGO) allow the compiler to see across translation units and profile hot paths:
- Function cloning / specialization heuristics (e.g., creating custom copies of
set_do_lookupwith resolved function pointers) are highly compiler-dependent and fragile. - By adding
Py_ALWAYS_INLINE, we guarantee that all compilers on all platforms (GCC, Clang, MSVC) specialize and inline the lookup logic consistently, making the performance gain reliable.
There was a problem hiding this comment.
Below is a response from an AI agent:
See our AI Policy, we expect PR authors and those filing issues to be able to explain their proposed changes in their own words.
There was a problem hiding this comment.
Mentioning Py_ALWAYS_INLINE explicitly enforces the compiler to inline the function. But if we don't specify, it is not guaranteed even with LTO and PGO.
Making it inline has a lot of benefit as it will inline set_compare_frozenset() call as well as the compiler knows statically which function is being called rather than linking it at runtime via pointer.
There was a problem hiding this comment.
Yes, it's not guaranteed, but modern compilers are pretty smart. Do new compilers already inline it?
There was a problem hiding this comment.
While it might be less common, when string membership lookups are performed on a frozenset, the fast path is highly effective.
In our PGO+LTO benchmarks, it resulted in a 21.1% speedup (reducing lookup time from 68.5 ms to 54.0 ms).
There was a problem hiding this comment.
Please post the benchmark script. This was considered before, and it was decided it's not worth it, you need to convince us we were wrong before. You also need to benchmark the two changes separately, and cases that aren't optimised to see what hit other paths take.
|
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 |
|
Please do not use the Update Branch button unless necessary (e.g. fixing conflicts, jogging the CI, or very old PRs) as it uses valuable resources. For more information see the devguide. |
|
Oh Ok, I will take care from next time. Here are the benchmark results run on a fresh PGO + LTO build (compiled with The benchmarks measure the time for 1,000,000 lookup operations (1,000 loops searching for 1,000 keys):
Specializing the comparison callback via |
|
I have made the requested changes; please review again. |
|
Thanks for making the requested changes! @StanFromIreland: please review the changes made to this pull request. |
I'm sorry but you have not, see my comments above. |
| } | ||
| Py_ssize_t ep_hash = ep->hash; | ||
| if (ep_hash == hash) { | ||
| if (PyUnicode_CheckExact(startkey) |
There was a problem hiding this comment.
question: why did you decide to use str as the fast path items here? what is so special about it?
I don't see how such change can make It was decided recently to not specialize I suggest closing this PR. |
Linked Issue
Fixes gh-153198
Proposed Changes
Objects/setobject.cto:PyUnicode_CheckExactandunicode_eq()fast-path check inset_compare_frozenset.set_do_lookupasstatic inline Py_ALWAYS_INLINE intso the compiler can specialize and inline the lookup loop for both comparison target functions.test_set(all 644 tests passed successfully).Benchmarks (1,000,000 lookup operations)
set_lookup(int keys): 20.1% faster (reduced from 42.2 ms to 33.7 ms)set_lookup_str(string keys): 26.2% faster (reduced from 71.1 ms to 52.5 ms)frozenset_lookup_str(string keys): 17.0% faster (reduced from 71.2 ms to 59.1 ms)