Skip to content

gh-153198 Optimize set and frozenset membership lookup#153199

Closed
rajat315315 wants to merge 5 commits into
python:mainfrom
rajat315315:set_membership_opt
Closed

gh-153198 Optimize set and frozenset membership lookup#153199
rajat315315 wants to merge 5 commits into
python:mainfrom
rajat315315:set_membership_opt

Conversation

@rajat315315

Copy link
Copy Markdown

Linked Issue

Fixes gh-153198

Proposed Changes

  • Modified Objects/setobject.c to:
    • Add PyUnicode_CheckExact and unicode_eq() fast-path check in set_compare_frozenset.
    • Mark set_do_lookup as static inline Py_ALWAYS_INLINE int so the compiler can specialize and inline the lookup loop for both comparison target functions.
  • Verified correctness by running 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)

@rajat315315 rajat315315 requested a review from rhettinger as a code owner July 6, 2026 14:17
@bedevere-app

bedevere-app Bot commented Jul 6, 2026

Copy link
Copy Markdown

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 skip news label instead.

@python-cla-bot

python-cla-bot Bot commented Jul 6, 2026

Copy link
Copy Markdown

All commit authors signed the Contributor License Agreement.

CLA signed

@rajat315315 rajat315315 changed the title Optimize set and frozenset membership lookup gh-153198 Optimize set and frozenset membership lookup Jul 6, 2026

@StanFromIreland StanFromIreland left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please provide benchmarks, run on a PGO + LTO build.

Comment thread Objects/setobject.c
#define PERTURB_SHIFT 5

static int
static inline Py_ALWAYS_INLINE int

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is the compiler not already specialising this?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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_lookup with 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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yes, it's not guaranteed, but modern compilers are pretty smart. Do new compilers already inline it?

Comment thread Objects/setobject.c Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

??

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

@bedevere-app

bedevere-app Bot commented Jul 6, 2026

Copy link
Copy Markdown

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 I have made the requested changes; please review again. I will then notify any core developers who have left a review that you're ready for them to take another look at this pull request.

@StanFromIreland StanFromIreland added the pending The issue will be closed if no feedback is provided label Jul 6, 2026
@StanFromIreland

Copy link
Copy Markdown
Member

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.

@rajat315315

Copy link
Copy Markdown
Author

Oh Ok, I will take care from next time.

Here are the benchmark results run on a fresh PGO + LTO build (compiled with --enable-optimizations --with-lto=full using GCC 15.2.0 on Linux).

The benchmarks measure the time for 1,000,000 lookup operations (1,000 loops searching for 1,000 keys):

Benchmark (1,000,000 lookups) Baseline (main) Optimized (this PR) Time Saved (%) Speedup
set_lookup_int 61.859 ms ± 16.041 ms 50.429 ms ± 15.375 ms 18.5% 1.23x
set_lookup_str 75.043 ms ± 16.397 ms 53.779 ms ± 9.788 ms 28.3% 1.40x
frozenset_lookup_str 68.524 ms ± 9.395 ms 54.071 ms ± 5.390 ms 21.1% 1.27x

Specializing the comparison callback via Py_ALWAYS_INLINE on set_do_lookup yields a ~18.5% improvement for integer set lookups, while adding the PyUnicode_CheckExact() and unicode_eq() fast path check in set_compare_frozenset improves string lookup performance by 21% to 28%.

@rajat315315

Copy link
Copy Markdown
Author

I have made the requested changes; please review again.

@bedevere-app

bedevere-app Bot commented Jul 7, 2026

Copy link
Copy Markdown

Thanks for making the requested changes!

@StanFromIreland: please review the changes made to this pull request.

@bedevere-app bedevere-app Bot requested a review from StanFromIreland July 7, 2026 09:52
@StanFromIreland

Copy link
Copy Markdown
Member

I have made the requested changes; please review again.

I'm sorry but you have not, see my comments above.

Comment thread Objects/setobject.c
}
Py_ssize_t ep_hash = ep->hash;
if (ep_hash == hash) {
if (PyUnicode_CheckExact(startkey)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

question: why did you decide to use str as the fast path items here? what is so special about it?

@vstinner

vstinner commented Jul 7, 2026

Copy link
Copy Markdown
Member

Optimize set and frozenset membership lookup by inlining set_do_lookup and adding a PyUnicode_CheckExact fast path to set_compare_frozenset.

I don't see how such change can make set faster for any type. I expect that it makes set slower for types other than str.

It was decided recently to not specialize set for str: see #137828 discussion.

I suggest closing this PR.

@sobolevn sobolevn closed this Jul 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting change review pending The issue will be closed if no feedback is provided

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Optimize set and frozenset membership lookup (x in s) performance

4 participants