Skip to content

Commit 8b9f2cf

Browse files
johngclaude
andcommitted
pythongh-143732: Specialize __setitem__ dunder method for STORE_SUBSCR
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>
1 parent 24e5a55 commit 8b9f2cf

26 files changed

Lines changed: 1123 additions & 272 deletions

‎Include/cpython/object.h‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,9 +266,18 @@ struct _specialization_cache {
266266
// - If getitem->func_version == getitem_version, then getitem can be called
267267
// with two positional arguments and no keyword arguments, and has neither
268268
// *args nor **kwargs (as required by BINARY_OP_SUBSCR_GETITEM):
269+
// The same contract applies to setitem, except that it is the Python
270+
// function that PyType_Lookup(cls, "__setitem__") would return, and it is
271+
// called with three positional arguments (as required by
272+
// STORE_SUBSCR_PY_DUNDER).
273+
// The pointers are grouped ahead of the versions so that the two version
274+
// fields share one word of padding, which keeps this struct at 32 bytes
275+
// rather than 40.
269276
PyObject *getitem;
270-
uint32_t getitem_version;
277+
PyObject *setitem;
271278
PyObject *init;
279+
uint32_t getitem_version;
280+
uint32_t setitem_version;
272281
};
273282

274283
/* The *real* layout of a type object when allocated on the heap */

‎Include/internal/pycore_code.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,7 @@ extern PyObject *_PyInstrumentation_BranchesIterator(PyCodeObject *code);
549549
struct _PyCode8 _PyCode_DEF(8);
550550

551551
PyAPI_DATA(const struct _PyCode8) _Py_InitCleanup;
552+
PyAPI_DATA(const struct _PyCode8) _Py_SetItemCleanup;
552553

553554
#ifdef Py_GIL_DISABLED
554555

‎Include/internal/pycore_magic_number.h‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,7 @@ Known values:
303303
Python 3.16a1 3703 (Replace DELETE_GLOBAL with PUSH_NULL; STORE_GLOBAL)
304304
Python 3.16a1 3704 (Replace DELETE_ATTR with PUSH_NULL; STORE_ATTR)
305305
Python 3.16a1 3705 (Add INTRINSIC_ADD_CONDITIONAL_ANNOTATION)
306+
Python 3.16a1 3706 (Add STORE_SUBSCR_PY_DUNDER and EXIT_SETITEM)
306307
307308
Python 3.17 will start with 3750
308309
@@ -312,7 +313,7 @@ Known values:
312313
313314
*/
314315

315-
#define PYC_MAGIC_NUMBER 3705
316+
#define PYC_MAGIC_NUMBER 3706
316317
/* This is equivalent to converting PYC_MAGIC_NUMBER to 2 bytes
317318
(little-endian) and then appending b'\r\n'. */
318319
#define PYC_MAGIC_NUMBER_TOKEN \

‎Include/internal/pycore_opcode_metadata.h‎

Lines changed: 16 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Include/internal/pycore_typeobject.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ typedef int (*_py_validate_type)(PyTypeObject *);
164164
// Exported for external JIT support
165165
int _PyType_Validate(PyTypeObject *ty, _py_validate_type validate, unsigned int *tp_version);
166166
int _PyType_CacheGetItemForSpecialization(PyHeapTypeObject *ht, PyObject *descriptor, uint32_t tp_version);
167+
int _PyType_CacheSetItemForSpecialization(PyHeapTypeObject *ht, PyObject *descriptor, uint32_t tp_version);
167168

168169
// Precalculates count of non-unique slots and fills wrapperbase.name_count.
169170
extern int _PyType_InitSlotDefs(PyInterpreterState *interp);

‎Include/internal/pycore_uop_ids.h‎

Lines changed: 6 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Include/internal/pycore_uop_metadata.h‎

Lines changed: 30 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)