gh-143732: Specialize __setitem__ dunder method for STORE_SUBSCR - #2
Closed
johng wants to merge 1 commit into
Closed
gh-143732: Specialize __setitem__ dunder method for STORE_SUBSCR#2johng wants to merge 1 commit into
__setitem__ dunder method for STORE_SUBSCR#2johng wants to merge 1 commit into
Conversation
johng
force-pushed
the
gh-143732-store-subscr-py-dunder-v2
branch
3 times, most recently
from
August 28, 2026 11:28
7817364 to
8b9f2cf
Compare
…BSCR` Add STORE_SUBSCR_PY_DUNDER, which enters a Python `__setitem__` directly rather than calling it through PyObject_SetItem, following the template pythongh-143732 asks for. Unlike BINARY_OP_SUBSCR_GETITEM, the frame for the dunder cannot simply be pushed. _RETURN_VALUE is `(retval -- res)`, so a returning frame always pushes exactly one value onto the caller: BINARY_OP_SUBSCR_GETITEM is declared pops 2 / pushes 0 and the +1 arrives on return, netting -1 to match generic BINARY_OP. STORE_SUBSCR must net -3, so a frame-pushing member would pop 3, declare 0 pushed and gain +1 on return, netting -2. `__setitem__` returns a None that nothing consumes. So a shim frame (_Py_SetItemCleanup) is pushed underneath the `__setitem__` frame, in the same way CALL_ALLOC_AND_ENTER_INIT uses _Py_InitCleanup for `__init__`. The dunder returns into the shim, whose sole instruction EXIT_SETITEM discards that value and pops the shim without pushing a result, keeping the net stack effect at -3. The guard and the frame push are deliberately a single uop. Splitting them, mirroring _BINARY_OP_SUBSCR_CHECK_FUNC, puts the looked-up function on the caller's value stack as a temporary; STORE_SUBSCR already occupies three stack slots, so that extra slot overruns co_stacksize and trips ASSERT_WITHIN_STACK_BOUNDS. BINARY_OP_SUBSCR_GETITEM survives it because BINARY_OP occupies only two. The tier two abstract interpreter therefore recovers the function from the container's type via _spec_cache.setitem, exactly as _BINARY_OP_SUBSCR_CHECK_FUNC does, rather than from a stack symbol. Tier two support is included: EXIT_SETITEM reuses _GUARD_IP_RETURN_VALUE and _GUARD_CODE_VERSION_RETURN_VALUE because its post-pop state matches _RETURN_VALUE's, the shim predicate in optimizer_analysis.c is generalised to cover both trampolines, and _Py_SetItemCleanup is added to has_space_for_executor() so insert_executor() cannot write ENTER_EXECUTOR into a statically allocated const code object. Without this, a hot loop containing `x[k] = v` would end its trace at the store, which plain STORE_SUBSCR did not. The function is cached on the heap type (_spec_cache.setitem, mirroring _spec_cache.getitem) rather than in the inline cache, so STORE_SUBSCR's cache size is unchanged. The cost is paid in heap type size instead: the pointers in _specialization_cache are grouped ahead of the version fields so the two uint32_t versions share one word of padding, which holds the struct at 32 bytes rather than 40, i.e. 8 bytes per heap type rather than 16. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
johng
force-pushed
the
gh-143732-store-subscr-py-dunder-v2
branch
from
August 28, 2026 11:49
8b9f2cf to
926227d
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Reworked take on python#156033, which was closed with:
Single commit, rebased on upstream
24e5a55ccb. This fork'smainhas been fast-forwarded to the same commit, so the diff is exactly this change.(Supersedes #1, which targeted a temporary base branch and was auto-closed by the squash.)
Why the old approach didn't satisfy the issue
The previous PR called the dunder through an ordinary re-entrant vectorcall (
_PyObject_VectorcallTstate). pythongh-143732 asks these specializations to jump directly into the method, usingLOAD_ATTR_GETATTRIBUTE_OVERRIDDENas the template, so the JIT can trace through the call. A vectorcall gets the interpreter win but not the inlined frame, which is the actual point.The obstacle
You cannot simply push the
__setitem__frame the wayBINARY_OP_SUBSCR_GETITEMpushes__getitem__:_RETURN_VALUEis(retval -- res). A returning frame always pushes exactly one value onto the caller.BINARY_OP_SUBSCR_GETITEMis declared pops 2 / pushes 0; the +1 arrives on return, netting −1 to match genericBINARY_OP.STORE_SUBSCRmust net −3. A frame-pushing member pops 3, declares 0 pushed, gains +1 on return → −2. Off by one, with no fourth operand to absorb it.__setitem__returns aNonethat nothing consumes.Approach
A shim frame (
_Py_SetItemCleanup) is pushed underneath the__setitem__frame, exactly asCALL_ALLOC_AND_ENTER_INITuses_Py_InitCleanupfor__init__. The dunder returns into the shim; the shim's sole instructionEXIT_SETITEMdiscards that value and pops the shim without pushing, keeping the net effect at −3.The guard and frame push are deliberately a single uop. Splitting them, mirroring
_BINARY_OP_SUBSCR_CHECK_FUNC, puts the looked-up function on the caller's value stack as a temporary;STORE_SUBSCRalready occupies three stack slots, so that extra slot overrunsco_stacksizeand tripsASSERT_WITHIN_STACK_BOUNDS:BINARY_OP_SUBSCR_GETITEMsurvives it becauseBINARY_OPoccupies only two. Worth knowing this is invisible to both a release build and the code generators — only--with-pydebugcatches it.The function is cached on the heap type (
_spec_cache.setitem, mirroring_spec_cache.getitem) rather than in the inline cache, soSTORE_SUBSCR's cache size stays at 1 — the old PR grew it to 3, bloating everySTORE_SUBSCR.Tier 1 only; tier 2 / JIT support is deliberately left for a follow-up.
executor_cases.c.h,optimizer_cases.c.handpycore_uop_ids.hare untouched.Testing
Built
--with-pydebugand verified:test_opcachec[i] = v→STORE_SUBSCR_PY_DUNDER['<module>', '__setitem__']— shim invisiblesys.settrace__setitem____setitem__RecursionError*args/ defaults / kw-onlyThat last row is why
assert(fcode->co_argcount == 3)is an assert rather than a deopt, matching_BINARY_OP_SUBSCR_CHECK_FUNC.Open items before this goes upstream
__setitem__dunder method forSTORE_SUBSCRpython/cpython#156033 is unverified. A release build + thepyperfscript is needed.__dunder__fallback specialization; not included.🤖 Generated with Claude Code