Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Optimize set and frozenset membership lookup by inlining ``set_do_lookup`` and adding a ``PyUnicode_CheckExact`` fast path to ``set_compare_frozenset``.
10 changes: 7 additions & 3 deletions Objects/setobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,7 @@ set_compare_entry_lock_held(PySetObject *so, setentry *table, setentry *entry,

// This is similar to set_compare_entry_lock_held() but we don't need to
// incref startkey before comparing and we don't need to check if the set has
// changed. This also omits the PyUnicode_CheckExact() special case since it
// doesn't help much for frozensets.
// changed.
static inline Py_ALWAYS_INLINE int
set_compare_frozenset(PySetObject *so, setentry *table, setentry *ep,
PyObject *key, Py_hash_t hash)
Expand All @@ -182,6 +181,11 @@ set_compare_frozenset(PySetObject *so, setentry *table, setentry *ep,
}
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?

&& PyUnicode_CheckExact(key)
&& unicode_eq(startkey, key)) {
return SET_LOOKKEY_FOUND;
}
int cmp = PyObject_RichCompareBool(startkey, key, Py_EQ);
if (cmp < 0) {
return SET_LOOKKEY_ERROR;
Expand Down Expand Up @@ -217,7 +221,7 @@ set_zero_table(setentry *table, size_t size)
/* This must be >= 1 */
#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?

set_do_lookup(PySetObject *so, setentry *table, size_t mask, PyObject *key,
Py_hash_t hash, setentry **epp, compare_func compare_entry)
{
Expand Down
Loading