Skip to content

gh-149180: Avoid double checking tp_as_number, tp_as_sequence, tp_as_mapping#149317

Open
anujbharambe wants to merge 8 commits intopython:mainfrom
anujbharambe:gh-149180-empty-tp-as-structs
Open

gh-149180: Avoid double checking tp_as_number, tp_as_sequence, tp_as_mapping#149317
anujbharambe wants to merge 8 commits intopython:mainfrom
anujbharambe:gh-149180-empty-tp-as-structs

Conversation

@anujbharambe
Copy link
Copy Markdown
Contributor

gh-149180: Avoid double checking tp_as_number, tp_as_sequence, tp_as_mapping

During PyType_Ready, assign NULL tp_as_number, tp_as_sequence, and tp_as_mapping pointers to shared static empty (all-zero) structs. After initialization, these three fields are guaranteed non-NULL for all ready types.

This eliminates the need for callers to double-check — first the struct pointer, then the slot within — e.g.:

// Before: two checks needed
if (Py_TYPE(s)->tp_as_mapping && Py_TYPE(s)->tp_as_mapping->mp_length)

// After: only the slot check is needed (callsite cleanup is a follow-up)
if (Py_TYPE(s)->tp_as_mapping->mp_length)

Changes

Objects/typeobject.c

  • Added three static zero-initialized empty structs (_Py_empty_number_methods, _Py_empty_sequence_methods, _Py_empty_mapping_methods).
  • In type_ready_inherit(), after type_ready_inherit_as_structs(), assign the empty structs as fallbacks when tp_as_number, tp_as_sequence, or tp_as_mapping is still NULL. Placed outside the if (base != NULL) block so it covers PyBaseObject_Type.
  • Added CHECK() assertions in _PyType_CheckConsistency() to enforce the non-NULL invariant for ready types.
  • Added basebase == NULL guards in inherit_slots() before dereferencing basebase->tp_as_number, basebase->tp_as_sequence, and basebase->tp_as_mapping. Previously these were unreachable because the outer if checked base->tp_as_number != NULL which was false for object. Now that the empty struct makes it non-NULL, base->tp_base (NULL for object) must be guarded.

Objects/abstract.c

  • In PyNumber_InPlaceMultiply, changed else if (mw != NULL) to if (mw != NULL) in the sequence-repeat fallback. Previously int's tp_as_sequence was NULL, so the first branch was skipped and the else if handled the right operand (e.g. 2 *= [1]). Now that int has a non-NULL empty tp_as_sequence, the first branch is entered but finds no slots; removing the else allows the second branch to still run.

Safety

  • inherit_slots(): Runs before our fallback. The basebase NULL guards preserve existing behavior — all slots in the empty struct are NULL, so SLOTDEFINED returns false.
  • Heap types: Already have non-NULL tp_as_* (set by type_new_alloc()). The fallback never triggers.
  • Existing tp_as_* != NULL guards at callsites: Become always-true for ready types, which is harmless. The inner slot check still correctly fails via the empty struct's NULL slots.

cc- @markshannon

@read-the-docs-community
Copy link
Copy Markdown

read-the-docs-community Bot commented May 3, 2026

Documentation build overview

📚 cpython-previews | 🛠️ Build #32521024 | 📁 Comparing 6c11104 against main (8a7edda)

  🔍 Preview build  

8 files changed · ± 8 modified

± Modified

Comment thread Objects/abstract.c
if (f != NULL)
return sequence_repeat(f, v, w);
}
else if (mw != NULL) {
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.

I don't understand this branch change.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated, the new version removes all dead NULL checks on tp_as_number, tp_as_sequence, and tp_as_mapping throughout the file. They're now guaranteed non-NULL after PyType_Ready.

@bedevere-app
Copy link
Copy Markdown

bedevere-app Bot commented May 3, 2026

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.

Comment thread Objects/typeobject.c Outdated
Comment on lines +9162 to +9164
static PyNumberMethods _Py_empty_number_methods = {0};
static PySequenceMethods _Py_empty_sequence_methods = {0};
static PyMappingMethods _Py_empty_mapping_methods = {0};
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.

Those structs can be declared constants as well.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done, I have declared them static const.

Comment thread Objects/typeobject.c
if (type->tp_as_mapping != NULL && base->tp_as_mapping != NULL) {
basebase = base->tp_base;
if (basebase->tp_as_mapping == NULL)
if (basebase == NULL || basebase->tp_as_mapping == NULL)
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.

When is it possible for basebase to be NULL?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

basebase is base->tp_base, which is NULL when base is object (the root type has no parent). I have added a comment explaining this.

Copy link
Copy Markdown
Member

@picnixz picnixz left a comment

Choose a reason for hiding this comment

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

Please make some benchmarks as well to see how much we gain (probably macro benchmarks from pyperformance for that).

I think there are other places where we have LHS/RHS being tested so it may require more than just the change in PyNumber_Multiply (e.g., C classes that have implement their own slots).

AFAIU, all tp_as_sequence are no more NULLs for PyNumber_InPlaceMultiply or am I wrong here?

@picnixz
Copy link
Copy Markdown
Member

picnixz commented May 3, 2026

Also, don't use LLMs for your PRs unless you at least disclose its usage and to understand what has been written by you or it. See our policy: https://devguide.python.org/getting-started/generative-ai/

Copy link
Copy Markdown
Contributor

@NekoAsakura NekoAsakura left a comment

Choose a reason for hiding this comment

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

Please read requirements in the issue carefully.

Comment thread Objects/abstract.c Outdated
return sequence_repeat(f, v, w);
}
else if (mw != NULL) {
if (mw != NULL) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This would remove the need for the extra check

This is a dead check, exactly the kind of "extra check" the issue is asking to remove. There's a fair bit more like it that's been left in.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks for catching this. The latest push removes all dead NULL checks across the affected files.

Comment thread Objects/typeobject.c Outdated
Comment on lines +9162 to +9164
static PyNumberMethods _Py_empty_number_methods = {0};
static PySequenceMethods _Py_empty_sequence_methods = {0};
static PyMappingMethods _Py_empty_mapping_methods = {0};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Immutable types can all share common constant structs, so this should use very little extra memory.

…tp_as_mapping

During PyType_Ready, assign NULL tp_as_number, tp_as_sequence, and
tp_as_mapping pointers to shared static const empty (all-zero) structs.
After initialization, these three fields are guaranteed non-NULL for all
ready types.

Remove redundant NULL checks at all callsites across abstract.c,
object.c, bytesobject.c, complexobject.c, floatobject.c, typeobject.c,
_bisectmodule.c, and pycore_abstract.h.
@anujbharambe anujbharambe force-pushed the gh-149180-empty-tp-as-structs branch from 2abdd22 to b349bd7 Compare May 4, 2026 04:58
@sergey-miryanov
Copy link
Copy Markdown
Contributor

I recommend adding assertions to guarantee Py_TYPE(o) does not return NULL.

@anujbharambe
Copy link
Copy Markdown
Contributor Author

Py_TYPE(o) can never be NULL for a valid Python object, so assertions for it aren't needed here.

@sergey-miryanov
Copy link
Copy Markdown
Contributor

Sorry, I meant for cases like Py_TYPE(args)->tp_as_mapping and Py_TYPE(arg)->tp_as_number. The goal is to ensure future changes don't break existing code, even if it's not directly related.

@anujbharambe
Copy link
Copy Markdown
Contributor Author

This is already covered by assertions I added in _PyType_CheckConsistency(). Do you think we should still add them at callsites?

@sergey-miryanov
Copy link
Copy Markdown
Contributor

Maybe it is worth to add macro like Py_TYPE_AS_NUMBER that do assertion and returns Py_TYPE(o)->tp_as_number.

@picnixz WDYT?

@anujbharambe
Copy link
Copy Markdown
Contributor Author

@picnixz Benchmarks as requested.

Benchmark results (pyperformance)

Ran 10 benchmarks on macOS (Apple Silicon, arm64, 12 logical CPUs).
Both builds used ./configure without --enable-optimizations.

### chaos ###
Mean +- std dev: 30.9 ms +- 0.8 ms -> 31.0 ms +- 0.8 ms: 1.01x slower (Not significant)

### fannkuch ###
Mean +- std dev: 186 ms +- 4 ms -> 185 ms +- 2 ms: 1.01x faster (Not significant)

### float ###
Mean +- std dev: 37.6 ms +- 0.9 ms -> 37.2 ms +- 1.1 ms: 1.01x faster (Not significant)

### json_dumps ###
Mean +- std dev: 4.87 ms +- 0.08 ms -> 4.84 ms +- 0.03 ms: 1.01x faster (Not significant)

### json_loads ###
Mean +- std dev: 16.4 us +- 0.4 us -> 16.2 us +- 0.3 us: 1.01x faster (Not significant)

### nbody ###
Mean +- std dev: 53.2 ms +- 0.5 ms -> 53.5 ms +- 0.6 ms: 1.00x slower (Not significant)

### pidigits ###
Mean +- std dev: 179 ms +- 1 ms -> 178 ms +- 2 ms: 1.00x faster (Not significant)

### regex_compile ###
Mean +- std dev: 54.8 ms +- 0.9 ms -> 55.2 ms +- 1.2 ms: 1.01x slower (Not significant)

### richards ###
Mean +- std dev: 25.2 ms +- 0.5 ms -> 25.3 ms +- 0.5 ms: 1.01x slower (Not significant)

### spectral_norm ###
Mean +- std dev: 51.7 ms +- 1.1 ms -> 52.2 ms +- 0.4 ms: 1.01x slower (Not significant)

No measurable regression. All differences are within noise (<1%).
The primary benefit of this change is code simplification, not raw speed.

AI disclosure: This PR was developed with assistance from Claude Code (Anthropic) for code changes and benchmarking, in accordance with the generative AI policy. All changes were reviewed and understood before committing.

@anujbharambe
Copy link
Copy Markdown
Contributor Author

I have made the requested changes; please review again.

@bedevere-app
Copy link
Copy Markdown

bedevere-app Bot commented May 4, 2026

Thanks for making the requested changes!

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

@bedevere-app bedevere-app Bot requested a review from picnixz May 4, 2026 08:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants