Conversation
…BSCR` Add STORE_SUBSCR_PY_DUNDER, which enters a Python `__setitem__` directly instead of calling it through PyObject_SetItem, following the template the issue asks for. Unlike BINARY_OP_SUBSCR_GETITEM, the frame for the dunder cannot simply be pushed: STORE_SUBSCR must pop three operands and push nothing, but a returning frame always pushes exactly one value (see _RETURN_VALUE). So a shim frame (_Py_SetItemCleanup) is pushed underneath the `__setitem__` frame, in the same way CALL_ALLOC_AND_ENTER_INIT uses _Py_InitCleanup. `__setitem__` returns into the shim, whose EXIT_SETITEM discards the returned value and pops the shim without pushing a result, keeping the net stack effect at -3. The function is cached on the heap type's specialization cache (_spec_cache.setitem) rather than in the inline cache, mirroring _spec_cache.getitem, so STORE_SUBSCR's cache size is unchanged. This is tier 1 only; tier 2 / JIT support is left for a follow-up. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Splitting the guard and the frame push into two uops (mirroring _BINARY_OP_SUBSCR_CHECK_FUNC) pushed the looked-up function onto the caller's value stack as a temporary. STORE_SUBSCR already occupies three stack slots, so the extra slot overran the frame's co_stacksize and tripped ASSERT_WITHIN_STACK_BOUNDS in a debug build. BINARY_OP_SUBSCR_GETITEM gets away with this because BINARY_OP only occupies two slots. Merge the guard and the frame push into a single uop so the function stays in a C local. This is fine here because the specialization is tier 1 only and does not need a separately traceable guard uop. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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:
Review base: this targets
base-gh-143732, a branch pinned at upstream24e5a55ccb, so the diff is exactly the two commits here.mainon this fork is 579 commits behind upstream and would have buried the change.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 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.Second commit
The first commit split guard and frame-push into two uops, mirroring
_BINARY_OP_SUBSCR_CHECK_FUNC. That pushes the looked-up function onto the caller's stack as a temporary, which overranco_stacksizeand trippedASSERT_WITHIN_STACK_BOUNDS:BINARY_OP_SUBSCR_GETITEMsurvives that extra slot becauseBINARY_OPoccupies two;STORE_SUBSCRoccupies three. The second commit merges the uops so the function stays in a C local. Kept as a separate commit because it is the one non-obvious constraint here — squash before submitting upstream.Note this was invisible to both a release build and the code generators; only
--with-pydebugcaught it.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
__setitem__dunder method forSTORE_SUBSCRpython/cpython#156033 is unverified here. A release build + thepyperfscript is needed before submitting upstream.__dunder__fallback specialization; not included.🤖 Generated with Claude Code