Repository: python/cpython
Pull request #133396: gh-133395: add option for extension modules to specialize BINARY_OP/SUBSCR, apply to arrays
Author: login: iritkatriel
Created: 2025-05-04T19:25:07Z | Merged: 2025-05-05T16:46:56Z

Benchmark results: https://github.com/faster-cpython/benchmarking-public/blob/main/results/bm-20250502-3.14.0a7%2B-ec344eb/bm-20250502-azure-x86_64-iritkatriel-array_subscr-3.14.0a7%2B-ec344eb-pystats-vs-base.md

Specialization stats reflect the change, no overall improvement in perf.


<!--
Thanks for your contribution!
Please read this comment in its entirety. It's quite important.

# Pull Request title

It should be in the following format:

```
gh-NNNNNN: Summary of the changes made
```

Where: gh-NNNNNN refers to the GitHub issue number.

Most PRs will require an issue number. Trivial changes, like fixing a typo, do not need an issue.

# Backport Pull Request title

If this is a backport PR (PR made against branches other than `main`),
please ensure that the PR title is in the following format:

```
[X.Y] <title from the original PR> (GH-NNNNNN)
```

Where: [X.Y] is the branch name, for example: [3.13].

GH-NNNNNN refers to the PR number from `main`.

-->


<!-- gh-issue-number: gh-133395 -->
* Issue: gh-133395
<!-- /gh-issue-number -->



Diff:
diff --git a/Include/cpython/object.h b/Include/cpython/object.h
index 3a4d65f7712c63..818fc7d05605ff 100644
--- a/Include/cpython/object.h
+++ b/Include/cpython/object.h
@@ -143,6 +143,11 @@ typedef struct {
  * backwards-compatibility */
 typedef Py_ssize_t printfunc;
 
+/* Specialize a binary op by setting the descriptor pointer */
+struct _PyBinopSpecializationDescr;
+typedef int (*binop_specialize_func)(PyObject *v, PyObject *w, int oparg,
+                                    struct _PyBinopSpecializationDescr **descr);
+
 // If this structure is modified, Doc/includes/typestruct.h should be updated
 // as well.
 struct _typeobject {
@@ -233,6 +238,13 @@ struct _typeobject {
     /* bitset of which type-watchers care about this type */
     unsigned char tp_watched;
 
+    /* callback that may specialize BINARY_OP
+     * this is an experimental API based on the ideas in the paper
+     * Cross Module Quickening - The Curious Case of C Extensions
+     * by Felix Berlakovich and Stefan Brunthaler.
+     */
+    binop_specialize_func tp_binop_specialize;
+
     /* Number of tp_version_tag values used.
      * Set to _Py_ATTR_CACHE_UNUSED if the attribute cache is
      * disabled for this type (e.g. due to custom MRO entries).
diff --git a/Include/internal/pycore_code.h b/Include/internal/pycore_code.h
index 635d2b24f4bdff..1c77730e417ef7 100644
--- a/Include/internal/pycore_code.h
+++ b/Include/internal/pycore_code.h
@@ -482,13 +482,18 @@ adaptive_counter_backoff(_Py_BackoffCounter counter) {
 /* Specialization Extensions */
 
 /* callbacks for an external specialization */
+
+struct _PyBinopSpecializationDescr;
+
 typedef int (*binaryopguardfunc)(PyObject *lhs, PyObject *rhs);
-typedef PyObject *(*binaryopactionfunc)(PyObject *lhs, PyObject *rhs);
+typedef PyObject* (*binaryopactionfunc)(PyObject *lhs, PyObject *rhs);
+typedef void (*binaryopfreefunc)(struct _PyBinopSpecializationDescr *descr);
 
-typedef struct {
+typedef struct _PyBinopSpecializationDescr {
     int oparg;
     binaryopguardfunc guard;
     binaryopactionfunc action;
+    binaryopfreefunc free;
 } _PyBinaryOpSpecializationDescr;
 
 /* Comparison bit masks. */
diff --git a/Include/internal/pycore_opcode_metadata.h b/Include/internal/pycore_opcode_metadata.h
index dc7ecc998c4477..fbb696e1755590 100644
--- a/Include/internal/pycore_opcode_metadata.h
+++ b/Include/internal/pycore_opcode_metadata.h
@@ -1082,7 +1082,7 @@ const struct opcode_metadata _PyOpcode_opcode_metadata[267] = {
     [BINARY_OP_ADD_FLOAT] = { true, INSTR_FMT_IXC0000, HAS_EXIT_FLAG | HAS_ERROR_FLAG },
     [BINARY_OP_ADD_INT] = { true, INSTR_FMT_IXC0000, HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG },
     [BINARY_OP_ADD_UNICODE] = { true, INSTR_FMT_IXC0000, HAS_EXIT_FLAG | HAS_ERROR_FLAG },
-    [BINARY_OP_EXTEND] = { true, INSTR_FMT_IXC0000, HAS_DEOPT_FLAG | HAS_ESCAPES_FLAG },
+    [BINARY_OP_EXTEND] = { true, INSTR_FMT_IXC0000, HAS_DEOPT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG },
     [BINARY_OP_INPLACE_ADD_UNICODE] = { true, INSTR_FMT_IXC0000, HAS_LOCAL_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG },
     [BINARY_OP_MULTIPLY_FLOAT] = { true, INSTR_FMT_IXC0000, HAS_EXIT_FLAG | HAS_ERROR_FLAG },
     [BINARY_OP_MULTIPLY_INT] = { true, INSTR_FMT_IXC0000, HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG },
@@ -1333,7 +1333,6 @@ _PyOpcode_macro_expansion[256] = {
     [BINARY_OP_ADD_FLOAT] = { .nuops = 3, .uops = { { _GUARD_TOS_FLOAT, OPARG_SIMPLE, 0 }, { _GUARD_NOS_FLOAT, OPARG_SIMPLE, 0 }, { _BINARY_OP_ADD_FLOAT, OPARG_SIMPLE, 5 } } },
     [BINARY_OP_ADD_INT] = { .nuops = 3, .uops = { { _GUARD_TOS_INT, OPARG_SIMPLE, 0 }, { _GUARD_NOS_INT, OPARG_SIMPLE, 0 }, { _BINARY_OP_ADD_INT, OPARG_SIMPLE, 5 } } },
     [BINARY_OP_ADD_UNICODE] = { .nuops = 3, .uops = { { _GUARD_TOS_UNICODE, OPARG_SIMPLE, 0 }, { _GUARD_NOS_UNICODE, OPARG_SIMPLE, 0 }, { _BINARY_OP_ADD_UNICODE, OPARG_SIMPLE, 5 } } },
-    [BINARY_OP_EXTEND] = { .nuops = 2, .uops = { { _GUARD_BINARY_OP_EXTEND, 4, 1 }, { _BINARY_OP_EXTEND, 4, 1 } } },
     [BINARY_OP_INPLACE_ADD_UNICODE] = { .nuops = 3, .uops = { { _GUARD_TOS_UNICODE, OPARG_SIMPLE, 0 }, { _GUARD_NOS_UNICODE, OPARG_SIMPLE, 0 }, { _BINARY_OP_INPLACE_ADD_UNICODE, OPARG_SIMPLE, 5 } } },
     [BINARY_OP_MULTIPLY_FLOAT] = { .nuops = 3, .uops = { { _GUARD_TOS_FLOAT, OPARG_SIMPLE, 0 }, { _GUARD_NOS_FLOAT, OPARG_SIMPLE, 0 }, { _BINARY_OP_MULTIPLY_FLOAT, OPARG_SIMPLE, 5 } } },
     [BINARY_OP_MULTIPLY_INT] = { .nuops = 3, .uops = { { _GUARD_TOS_INT, OPARG_SIMPLE, 0 }, { _GUARD_NOS_INT, OPARG_SIMPLE, 0 }, { _BINARY_OP_MULTIPLY_INT, OPARG_SIMPLE, 5 } } },
diff --git a/Include/internal/pycore_uop_metadata.h b/Include/internal/pycore_uop_metadata.h
index 9b88763da07ba7..922d8a6cadf08c 100644
--- a/Include/internal/pycore_uop_metadata.h
+++ b/Include/internal/pycore_uop_metadata.h
@@ -94,8 +94,7 @@ const uint16_t _PyUop_Flags[MAX_UOP_ID+1] = {
     [_BINARY_OP_SUBTRACT_FLOAT] = HAS_ERROR_FLAG | HAS_PURE_FLAG,
     [_BINARY_OP_ADD_UNICODE] = HAS_ERROR_FLAG | HAS_PURE_FLAG,
     [_BINARY_OP_INPLACE_ADD_UNICODE] = HAS_LOCAL_FLAG | HAS_DEOPT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG,
-    [_GUARD_BINARY_OP_EXTEND] = HAS_DEOPT_FLAG | HAS_ESCAPES_FLAG,
-    [_BINARY_OP_EXTEND] = HAS_ESCAPES_FLAG | HAS_PURE_FLAG,
+    [_BINARY_OP_EXTEND] = HAS_ERROR_FLAG | HAS_ESCAPES_FLAG | HAS_PURE_FLAG,
     [_BINARY_SLICE] = HAS_ERROR_FLAG | HAS_ESCAPES_FLAG,
     [_STORE_SLICE] = HAS_ERROR_FLAG | HAS_ESCAPES_FLAG,
     [_BINARY_OP_SUBSCR_LIST_INT] = HAS_DEOPT_FLAG | HAS_ESCAPES_FLAG,
@@ -423,7 +422,6 @@ const char *const _PyOpcode_uop_name[MAX_UOP_ID+1] = {
     [_GET_ITER] = "_GET_ITER",
     [_GET_LEN] = "_GET_LEN",
     [_GET_YIELD_FROM_ITER] = "_GET_YIELD_FROM_ITER",
-    [_GUARD_BINARY_OP_EXTEND] = "_GUARD_BINARY_OP_EXTEND",
     [_GUARD_CALLABLE_STR_1] = "_GUARD_CALLABLE_STR_1",
     [_GUARD_CALLABLE_TUPLE_1] = "_GUARD_CALLABLE_TUPLE_1",
     [_GUARD_CALLABLE_TYPE_1] = "_GUARD_CALLABLE_TYPE_1",
@@ -760,8 +758,6 @@ int _PyUop_num_popped(int opcode, int oparg)
             return 2;
         case _BINARY_OP_INPLACE_ADD_UNICODE:
             return 2;
-        case _GUARD_BINARY_OP_EXTEND:
-            return 0;
         case _BINARY_OP_EXTEND:
             return 2;
         case _BINARY_SLICE:
diff --git a/Include/typeslots.h b/Include/typeslots.h
index a7f3017ec02e92..980e714714e786 100644
--- a/Include/typeslots.h
+++ b/Include/typeslots.h
@@ -93,4 +93,5 @@
 #if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030E0000
 /* New in 3.14 */
 #define Py_tp_token 83
+#define Py_tp_binop_specialize 84
 #endif
diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py
index b3dba9dad006ab..f88763f35ec0ce 100644
--- a/Lib/test/test_sys.py
+++ b/Lib/test/test_sys.py
@@ -1776,7 +1776,7 @@ def delx(self): del self.__x
         check((1,2,3), vsize('') + self.P + 3*self.P)
         # type
         # static type: PyTypeObject
-        fmt = 'P2nPI13Pl4Pn9Pn12PIPc'
+        fmt = 'P2nPI13Pl4Pn9Pn12PI3Pc'
         s = vsize(fmt)
         check(int, s)
         typeid = 'n' if support.Py_GIL_DISABLED else ''
diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-05-04-19-32-47.gh-issue-133395.VhWWEP.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-05-04-19-32-47.gh-issue-133395.VhWWEP.rst
new file mode 100644
index 00000000000000..a391ce16339c26
--- /dev/null
+++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-05-04-19-32-47.gh-issue-133395.VhWWEP.rst
@@ -0,0 +1,2 @@
+Add option for extension modules to specialize ``BINARY_OP`` instructions.
+Applied to ``array`` objects.
diff --git a/Modules/arraymodule.c b/Modules/arraymodule.c
index 401a3a7072b846..4d2ff32cabe467 100644
--- a/Modules/arraymodule.c
+++ b/Modules/arraymodule.c
@@ -14,6 +14,8 @@
 #include "pycore_modsupport.h"    // _PyArg_NoKeywords()
 #include "pycore_moduleobject.h"  // _PyModule_GetState()
 
+#include "opcode.h"               // binary op opargs (NB_*)
+
 #include <stddef.h>               // offsetof()
 #include <stdbool.h>
 
@@ -848,6 +850,10 @@ array_richcompare(PyObject *v, PyObject *w, int op)
     return res;
 }
 
+static int
+array_binop_specialize(PyObject *v, PyObject *w, int oparg,
+                       _PyBinaryOpSpecializationDescr **descr);
+
 static Py_ssize_t
 array_length(PyObject *op)
 {
@@ -2963,6 +2969,8 @@ static PyType_Slot array_slots[] = {
     {Py_tp_alloc, PyType_GenericAlloc},
     {Py_tp_new, array_new},
     {Py_tp_traverse, array_tp_traverse},
+    {Py_tp_token, Py_TP_USE_SPEC},
+    {Py_tp_binop_specialize, array_binop_specialize},
 
     /* as sequence */
     {Py_sq_length, array_length},
@@ -2995,6 +3003,70 @@ static PyType_Spec array_spec = {
     .slots = array_slots,
 };
 
+static inline int
+array_subscr_guard(PyObject *lhs, PyObject *rhs)
+{
+    PyObject *exc = PyErr_GetRaisedException();
+    int ret = PyType_GetBaseByToken(Py_TYPE(lhs), &array_spec, NULL);
+    if (ret < 0) {
+        if (PyErr_ExceptionMatches(PyExc_TypeError)) {
+            PyErr_Clear();
+            ret = 0;
+        }
+    }
+    _PyErr_ChainExceptions1(exc);
+    return ret;
+}
+
+static PyObject *
+array_subscr_action(PyObject *lhs, PyObject *rhs)
+{
+    return array_subscr(lhs, rhs);
+}
+
+static void
+array_subscr_free(_PyBinaryOpSpecializationDescr* descr)
+{
+    if (descr != NULL) {
+        PyMem_Free(descr);
+    }
+}
+
+static int
+array_binop_specialize(PyObject *v, PyObject *w, int oparg,
+                       _PyBinaryOpSpecializationDescr **descr)
+{
+    array_state *state = find_array_state_by_type(Py_TYPE(v));
+
+    if (!array_Check(v, state)) {
+        return 0;
+    }
+
+    *descr = NULL;
+    switch(oparg) {
+        case NB_SUBSCR:
+            if (array_subscr_guard(v, w)) {
+                *descr = (_PyBinaryOpSpecializationDescr*)PyMem_Malloc(
+                             sizeof(_PyBinaryOpSpecializationDescr));
+                if (*descr == NULL) {
+                    PyErr_NoMemory();
+                    return -1;
+                }
+                **descr = (_PyBinaryOpSpecializationDescr) {
+                    .oparg = oparg,
+                    .guard = array_subscr_guard,
+                    .action = array_subscr_action,
+                    .free = array_subscr_free,
+                };
+                return 1;
+            }
+            break;
+    }
+
+    return 0;
+}
+
+
 /*********************** Array Iterator **************************/
 
 /*[clinic input]
diff --git a/Objects/typeslots.inc b/Objects/typeslots.inc
index 642160fe0bd8bc..f197c3f5023670 100644
--- a/Objects/typeslots.inc
+++ b/Objects/typeslots.inc
@@ -82,3 +82,4 @@
 {offsetof(PyAsyncMethods, am_send), offsetof(PyTypeObject, tp_as_async)},
 {-1, offsetof(PyTypeObject, tp_vectorcall)},
 {-1, offsetof(PyHeapTypeObject, ht_token)},
+{-1, offsetof(PyTypeObject, tp_binop_specialize)},
diff --git a/Python/bytecodes.c b/Python/bytecodes.c
index 5a52efaaec8042..cc47e57175d727 100644
--- a/Python/bytecodes.c
+++ b/Python/bytecodes.c
@@ -801,9 +801,19 @@ dummy_func(
             PyObject *right_o = PyStackRef_AsPyObjectBorrow(right);
             _PyBinaryOpSpecializationDescr *d = (_PyBinaryOpSpecializationDescr*)descr;
             assert(INLINE_CACHE_ENTRIES_BINARY_OP == 5);
-            assert(d && d->guard);
+            assert(d);
+            assert(d->guard);
             int res = d->guard(left_o, right_o);
-            DEOPT_IF(!res);
+            ERROR_IF(res < 0);
+            if (res == 0) {
+                if (d->free) {
+                    d->free(d);
+                }
+                _PyBinaryOpCache *cache = (_PyBinaryOpCache *)(this_instr+1);
+                write_ptr(cache->external_cache, NULL);
+                this_instr->op.code = BINARY_OP;
+                DEOPT_IF(true);
+            }
         }
 
         pure op(_BINARY_OP_EXTEND, (descr/4, left, right -- res)) {
@@ -816,6 +826,7 @@ dummy_func(
 
             PyObject *res_o = d->action(left_o, right_o);
             DECREF_INPUTS();
+            ERROR_IF(res_o == NULL);
             res = PyStackRef_FromPyObjectSteal(res_o);
         }
 
diff --git a/Python/executor_cases.c.h b/Python/executor_cases.c.h
index 7f3c3141ad00b9..662e050c5c478a 100644
--- a/Python/executor_cases.c.h
+++ b/Python/executor_cases.c.h
@@ -1147,26 +1147,7 @@
             break;
         }
 
-        case _GUARD_BINARY_OP_EXTEND: {
-            _PyStackRef right;
-            _PyStackRef left;
-            right = stack_pointer[-1];
-            left = stack_pointer[-2];
-            PyObject *descr = (PyObject *)CURRENT_OPERAND0();
-            PyObject *left_o = PyStackRef_AsPyObjectBorrow(left);
-            PyObject *right_o = PyStackRef_AsPyObjectBorrow(right);
-            _PyBinaryOpSpecializationDescr *d = (_PyBinaryOpSpecializationDescr*)descr;
-            assert(INLINE_CACHE_ENTRIES_BINARY_OP == 5);
-            assert(d && d->guard);
-            _PyFrame_SetStackPointer(frame, stack_pointer);
-            int res = d->guard(left_o, right_o);
-            stack_pointer = _PyFrame_GetStackPointer(frame);
-            if (!res) {
-                UOP_STAT_INC(uopcode, miss);
-                JUMP_TO_JUMP_TARGET();
-            }
-            break;
-        }
+        /* _GUARD_BINARY_OP_EXTEND is not a viable micro-op for tier 2 because it uses the 'this_instr' variable */
 
         case _BINARY_OP_EXTEND: {
             _PyStackRef right;
@@ -1193,6 +1174,9 @@
             stack_pointer = _PyFrame_GetStackPointer(frame);
             stack_pointer += -2;
             assert(WITHIN_STACK_BOUNDS());
+            if (res_o == NULL) {
+                JUMP_TO_ERROR();
+            }
             res = PyStackRef_FromPyObjectSteal(res_o);
             stack_pointer[0] = res;
             stack_pointer += 1;
diff --git a/Python/generated_cases.c.h b/Python/generated_cases.c.h
index ee54b385b7064e..072951d2a5fcf6 100644
--- a/Python/generated_cases.c.h
+++ b/Python/generated_cases.c.h
@@ -283,14 +283,30 @@
                 PyObject *right_o = PyStackRef_AsPyObjectBorrow(right);
                 _PyBinaryOpSpecializationDescr *d = (_PyBinaryOpSpecializationDescr*)descr;
                 assert(INLINE_CACHE_ENTRIES_BINARY_OP == 5);
-                assert(d && d->guard);
+                assert(d);
+                assert(d->guard);
                 _PyFrame_SetStackPointer(frame, stack_pointer);
                 int res = d->guard(left_o, right_o);
                 stack_pointer = _PyFrame_GetStackPointer(frame);
-                if (!res) {
-                    UPDATE_MISS_STATS(BINARY_OP);
-                    assert(_PyOpcode_Deopt[opcode] == (BINARY_OP));
-                    JUMP_TO_PREDICTED(BINARY_OP);
+                if (res < 0) {
+                    JUMP_TO_LABEL(error);
+                }
+                if (res == 0) {
+                    if (d->free) {
+                        _PyFrame_SetStackPointer(frame, stack_pointer);
+                        d->free(d);
+                        stack_pointer = _PyFrame_GetStackPointer(frame);
+                    }
+                    _PyBinaryOpCache *cache = (_PyBinaryOpCache *)(this_instr+1);
+                    _PyFrame_SetStackPointer(frame, stack_pointer);
+                    write_ptr(cache->external_cache, NULL);
+                    stack_pointer = _PyFrame_GetStackPointer(frame);
+                    this_instr->op.code = BINARY_OP;
+                    if (true) {
+                        UPDATE_MISS_STATS(BINARY_OP);
+                        assert(_PyOpcode_Deopt[opcode] == (BINARY_OP));
+                        JUMP_TO_PREDICTED(BINARY_OP);
+                    }
                 }
             }
             /* Skip -4 cache entry */
@@ -315,6 +331,9 @@
                 stack_pointer = _PyFrame_GetStackPointer(frame);
                 stack_pointer += -2;
                 assert(WITHIN_STACK_BOUNDS());
+                if (res_o == NULL) {
+                    JUMP_TO_LABEL(error);
+                }
                 res = PyStackRef_FromPyObjectSteal(res_o);
             }
             stack_pointer[0] = res;
diff --git a/Python/optimizer_cases.c.h b/Python/optimizer_cases.c.h
index 3f91f7eefc7bb0..8e8b2ecfa5b0cf 100644
--- a/Python/optimizer_cases.c.h
+++ b/Python/optimizer_cases.c.h
@@ -554,9 +554,7 @@
             break;
         }
 
-        case _GUARD_BINARY_OP_EXTEND: {
-            break;
-        }
+        /* _GUARD_BINARY_OP_EXTEND is not a viable micro-op for tier 2 */
 
         case _BINARY_OP_EXTEND: {
             JitOptSymbol *res;
diff --git a/Python/specialize.c b/Python/specialize.c
index 59ec9a4cad6b9c..fe4a65ee5f8079 100644
--- a/Python/specialize.c
+++ b/Python/specialize.c
@@ -2534,7 +2534,7 @@ LONG_FLOAT_ACTION(compactlong_float_multiply, *)
 LONG_FLOAT_ACTION(compactlong_float_true_div, /)
 #undef LONG_FLOAT_ACTION
 
-static _PyBinaryOpSpecializationDescr binaryop_extend_descrs[] = {
+static const _PyBinaryOpSpecializationDescr binaryop_extend_builtins[] = {
     /* long-long arithmetic */
     {NB_OR, compactlongs_guard, compactlongs_or},
     {NB_AND, compactlongs_guard, compactlongs_and},
@@ -2560,14 +2560,41 @@ static int
 binary_op_extended_specialization(PyObject *lhs, PyObject *rhs, int oparg,
                                   _PyBinaryOpSpecializationDescr **descr)
 {
-    size_t n = sizeof(binaryop_extend_descrs)/sizeof(_PyBinaryOpSpecializationDescr);
-    for (size_t i = 0; i < n; i++) {
-        _PyBinaryOpSpecializationDescr *d = &binaryop_extend_descrs[i];
+    /* We are currently using this only for NB_SUBSCR, which is not
+     * commutative. Will need to revisit this function when we use
+     * this for operators which are.
+     */
+
+    typedef _PyBinaryOpSpecializationDescr descr_type;
+    size_t size = Py_ARRAY_LENGTH(binaryop_extend_builtins);
+    for (size_t i = 0; i < size; i++) {
+        descr_type *d = (descr_type *)&binaryop_extend_builtins[i];
+        assert(d != NULL);
+        assert(d->guard != NULL);
         if (d->oparg == oparg && d->guard(lhs, rhs)) {
             *descr = d;
             return 1;
         }
     }
+
+    PyTypeObject *lhs_type = Py_TYPE(lhs);
+    if (lhs_type->tp_binop_specialize != NULL) {
+        int ret = lhs_type->tp_binop_specialize(lhs, rhs, oparg, descr);
+        if (ret < 0) {
+            return -1;
+        }
+        if (ret == 1) {
+            if (*descr == NULL) {
+                PyErr_Format(
+                    PyExc_ValueError,
+                    "tp_binop_specialize of '%T' returned 1 with *descr == NULL",
+                    lhs);
+                return -1;
+            }
+            (*descr)->oparg = oparg;
+        }
+        return ret;
+    }
     return 0;
 }
 


Reviews:
user: login: markshannon
body: Is the `_PyBinopSpecializationDescr` supposed to be extensible?
If so we'll need a means for extensions to free the structs they have allocated. Or is the intention that they are statically allocated?
If they aren't extensible, maybe the VM should allocate them and clean them up?


state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T08:41:36Z
user: login: iritkatriel
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T09:23:28Z
user: login: picnixz
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T09:38:15Z
user: login: iritkatriel
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T11:16:31Z
user: login: picnixz
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T11:37:07Z
user: login: picnixz
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T11:41:19Z
user: login: eendebakpt
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T11:41:20Z
user: login: iritkatriel
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T11:43:36Z
user: login: iritkatriel
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T11:46:09Z
user: login: picnixz
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T11:50:20Z
user: login: iritkatriel
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T11:51:12Z
user: login: iritkatriel
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T12:27:22Z
user: login: markshannon
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T12:54:49Z
user: login: mdboom
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T13:29:33Z
user: login: brandtbucher
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T05:10:02Z
user: login: iritkatriel
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T07:54:06Z

Review comments:
diff_hunk: @@ -143,6 +143,11 @@ typedef struct {
  * backwards-compatibility */
 typedef Py_ssize_t printfunc;
 
+/* Specialize a binary by setting the descriptor pointer */
+struct _PyBinopSpecializationDescr;
+typedef int(*binop_specialize_func)(PyObject *v, PyObject *w, int oparg,
path: Include/cpython/object.h
user: login: picnixz
body: ```suggestion
typedef int (*binop_specialize_func)(PyObject *v, PyObject *w, int oparg,
```

created_at: 2025-05-05T08:26:30Z
author_association: MEMBER
diff_hunk: @@ -348,6 +348,8 @@ typedef int (*objobjproc)(PyObject *, PyObject *);
 typedef int (*visitproc)(PyObject *, void *);
 typedef int (*traverseproc)(PyObject *, visitproc, void *);
 
+struct _PyBinaryOpSpecializationDescr;
+typedef int (*binopspecfunc)(PyObject *lhs, PyObject *rhs, int oparg, struct _PyBinaryOpSpecializationDescr* descr);
path: Include/object.h
user: login: picnixz
body: Maybe use the same typedef name as the other?
created_at: 2025-05-05T08:26:56Z
author_association: MEMBER
diff_hunk: @@ -2560,14 +2560,26 @@ static int
 binary_op_extended_specialization(PyObject *lhs, PyObject *rhs, int oparg,
                                   _PyBinaryOpSpecializationDescr **descr)
 {
-    size_t n = sizeof(binaryop_extend_descrs)/sizeof(_PyBinaryOpSpecializationDescr);
-    for (size_t i = 0; i < n; i++) {
-        _PyBinaryOpSpecializationDescr *d = &binaryop_extend_descrs[i];
+    typedef _PyBinaryOpSpecializationDescr descr_type;
+    size_t size = sizeof(binaryop_extend_builtins)/sizeof(descr_type);
+    for (size_t i = 0; i < size; i++) {
+        descr_type *d = (descr_type *)&binaryop_extend_builtins[i];
+        assert(d != NULL);
+        assert(d->guard != NULL);
         if (d->oparg == oparg && d->guard(lhs, rhs)) {
             *descr = d;
             return 1;
         }
     }
+
+    if (Py_TYPE(lhs)->tp_binop_specialize != NULL) {
+        int ret = Py_TYPE(lhs)->tp_binop_specialize(lhs, rhs, oparg, descr);
+        if (ret < 0) {
+            return -1;
+        }
+
path: Python/specialize.c
user: login: markshannon
body: ```suggestion
     *descr->oparg = oparg;
```
I don't think we should rely on extensions to correctly set `oparg`.
created_at: 2025-05-05T08:33:42Z
author_association: MEMBER
diff_hunk: @@ -2995,6 +3003,61 @@ static PyType_Spec array_spec = {
     .slots = array_slots,
 };
 
+static inline int
+array_subscr_guard(PyObject *lhs, PyObject *rhs)
+{
+    PyObject *exc = PyErr_GetRaisedException();
+    int ret = PyType_GetBaseByToken(Py_TYPE(lhs), &array_spec, NULL);
+    if (ret < 0) {
+        if (PyErr_ExceptionMatches(PyExc_TypeError)) {
+            PyErr_Clear();
+            ret = 0;
+        }
+    }
+    _PyErr_ChainExceptions1(exc);
+    return ret;
+}
+
+static PyObject *
+array_subscr_action(PyObject *lhs, PyObject *rhs)
+{
+    return array_subscr(lhs, rhs);
+}
+
+static int
+array_binop_specialize(PyObject *v, PyObject *w, int oparg,
+                       _PyBinaryOpSpecializationDescr **descr)
+{
+    array_state *state = find_array_state_by_type(Py_TYPE(v));
+
+    if (!array_Check(v, state)) {
+        return 0;
+    }
+
+    *descr = NULL;
+    switch(oparg) {
+        case NB_SUBSCR:
+            if (array_subscr_guard(v, w)) {
+                *descr = (_PyBinaryOpSpecializationDescr*)PyMem_Malloc(
path: Modules/arraymodule.c
user: login: markshannon
body: Should the VM should be doing the memory allocation, not expecting extensions to do it?
That way we can be sure that the memory is freed when no longer needed.

How does this memory get freed?
I note that this PR has a `malloc` but no `free`.

created_at: 2025-05-05T08:33:48Z
author_association: MEMBER
diff_hunk: @@ -233,6 +238,9 @@ struct _typeobject {
     /* bitset of which type-watchers care about this type */
     unsigned char tp_watched;
 
+    /* callback that may specialize BINARY_OP */
+    binop_specialize_func tp_binop_specialize;
path: Include/cpython/object.h
user: login: markshannon
body: Is there a way to do this without adding a `tp_` slot?

Adding a "type slot" (`Py_tp_binop_specialize`) is fine, as it doesn't restrict how we implement it.
created_at: 2025-05-05T08:34:15Z
author_association: MEMBER
diff_hunk: @@ -2995,6 +3003,61 @@ static PyType_Spec array_spec = {
     .slots = array_slots,
 };
 
+static inline int
+array_subscr_guard(PyObject *lhs, PyObject *rhs)
+{
+    PyObject *exc = PyErr_GetRaisedException();
+    int ret = PyType_GetBaseByToken(Py_TYPE(lhs), &array_spec, NULL);
+    if (ret < 0) {
+        if (PyErr_ExceptionMatches(PyExc_TypeError)) {
+            PyErr_Clear();
+            ret = 0;
+        }
+    }
+    _PyErr_ChainExceptions1(exc);
+    return ret;
+}
+
+static PyObject *
+array_subscr_action(PyObject *lhs, PyObject *rhs)
+{
+    return array_subscr(lhs, rhs);
+}
+
+static int
+array_binop_specialize(PyObject *v, PyObject *w, int oparg,
+                       _PyBinaryOpSpecializationDescr **descr)
+{
+    array_state *state = find_array_state_by_type(Py_TYPE(v));
+
+    if (!array_Check(v, state)) {
+        return 0;
+    }
+
+    *descr = NULL;
+    switch(oparg) {
+        case NB_SUBSCR:
+            if (array_subscr_guard(v, w)) {
+                *descr = (_PyBinaryOpSpecializationDescr*)PyMem_Malloc(
path: Modules/arraymodule.c
user: login: iritkatriel
body: Yes, I forgot to add the free callback. 
created_at: 2025-05-05T09:23:27Z
author_association: MEMBER
diff_hunk: @@ -2995,6 +3003,61 @@ static PyType_Spec array_spec = {
     .slots = array_slots,
 };
 
+static inline int
+array_subscr_guard(PyObject *lhs, PyObject *rhs)
+{
+    PyObject *exc = PyErr_GetRaisedException();
+    int ret = PyType_GetBaseByToken(Py_TYPE(lhs), &array_spec, NULL);
+    if (ret < 0) {
+        if (PyErr_ExceptionMatches(PyExc_TypeError)) {
+            PyErr_Clear();
+            ret = 0;
+        }
+    }
+    _PyErr_ChainExceptions1(exc);
+    return ret;
+}
+
+static PyObject *
+array_subscr_action(PyObject *lhs, PyObject *rhs)
+{
+    return array_subscr(lhs, rhs);
+}
+
+static int
+array_binop_specialize(PyObject *v, PyObject *w, int oparg,
+                       _PyBinaryOpSpecializationDescr **descr)
+{
+    array_state *state = find_array_state_by_type(Py_TYPE(v));
+
+    if (!array_Check(v, state)) {
+        return 0;
+    }
+
+    *descr = NULL;
+    switch(oparg) {
+        case NB_SUBSCR:
+            if (array_subscr_guard(v, w)) {
path: Modules/arraymodule.c
user: login: picnixz
body: Would it make sense to implement the subscr logic in a separate function as well? 
created_at: 2025-05-05T09:30:22Z
author_association: MEMBER
diff_hunk: @@ -3252,7 +3315,6 @@ array_modexec(PyObject *m)
     if (PyModule_Add(m, "typecodes", typecodes) < 0) {
         return -1;
     }
-
path: Modules/arraymodule.c
user: login: picnixz
body: Typo?
created_at: 2025-05-05T09:32:07Z
author_association: MEMBER
diff_hunk: @@ -2560,14 +2560,35 @@ static int
 binary_op_extended_specialization(PyObject *lhs, PyObject *rhs, int oparg,
                                   _PyBinaryOpSpecializationDescr **descr)
 {
-    size_t n = sizeof(binaryop_extend_descrs)/sizeof(_PyBinaryOpSpecializationDescr);
-    for (size_t i = 0; i < n; i++) {
-        _PyBinaryOpSpecializationDescr *d = &binaryop_extend_descrs[i];
+    typedef _PyBinaryOpSpecializationDescr descr_type;
+    size_t size = sizeof(binaryop_extend_builtins)/sizeof(descr_type);
path: Python/specialize.c
user: login: picnixz
body: Don't we have some macro for this computation? Something like Py_ARRAY_SIZE? 
created_at: 2025-05-05T09:33:02Z
author_association: MEMBER
diff_hunk: @@ -2560,14 +2560,35 @@ static int
 binary_op_extended_specialization(PyObject *lhs, PyObject *rhs, int oparg,
                                   _PyBinaryOpSpecializationDescr **descr)
 {
-    size_t n = sizeof(binaryop_extend_descrs)/sizeof(_PyBinaryOpSpecializationDescr);
-    for (size_t i = 0; i < n; i++) {
-        _PyBinaryOpSpecializationDescr *d = &binaryop_extend_descrs[i];
+    typedef _PyBinaryOpSpecializationDescr descr_type;
+    size_t size = sizeof(binaryop_extend_builtins)/sizeof(descr_type);
+    for (size_t i = 0; i < size; i++) {
+        descr_type *d = (descr_type *)&binaryop_extend_builtins[i];
+        assert(d != NULL);
+        assert(d->guard != NULL);
         if (d->oparg == oparg && d->guard(lhs, rhs)) {
             *descr = d;
             return 1;
         }
     }
+
+    if (Py_TYPE(lhs)->tp_binop_specialize != NULL) {
path: Python/specialize.c
user: login: picnixz
body: Maybe store Py_TYPE(lhs) in a variable?
created_at: 2025-05-05T09:34:20Z
author_association: MEMBER
diff_hunk: @@ -2560,14 +2560,35 @@ static int
 binary_op_extended_specialization(PyObject *lhs, PyObject *rhs, int oparg,
                                   _PyBinaryOpSpecializationDescr **descr)
 {
-    size_t n = sizeof(binaryop_extend_descrs)/sizeof(_PyBinaryOpSpecializationDescr);
-    for (size_t i = 0; i < n; i++) {
-        _PyBinaryOpSpecializationDescr *d = &binaryop_extend_descrs[i];
+    typedef _PyBinaryOpSpecializationDescr descr_type;
+    size_t size = sizeof(binaryop_extend_builtins)/sizeof(descr_type);
+    for (size_t i = 0; i < size; i++) {
+        descr_type *d = (descr_type *)&binaryop_extend_builtins[i];
+        assert(d != NULL);
+        assert(d->guard != NULL);
         if (d->oparg == oparg && d->guard(lhs, rhs)) {
             *descr = d;
             return 1;
         }
     }
+
+    if (Py_TYPE(lhs)->tp_binop_specialize != NULL) {
+        int ret = Py_TYPE(lhs)->tp_binop_specialize(lhs, rhs, oparg, descr);
+        if (ret < 0) {
+            return -1;
+        }
+        if (ret == 1) {
+            if (*descr == NULL) {
+                PyErr_Format(
+                    PyExc_ValueError,
+                    "tp_binop_specialize of '%.200s' returned 1 with *descr == NULL",
path: Python/specialize.c
user: login: picnixz
body: Maybe use %T instead of '%.200s'?
created_at: 2025-05-05T09:34:43Z
author_association: MEMBER
diff_hunk: @@ -2560,14 +2560,35 @@ static int
 binary_op_extended_specialization(PyObject *lhs, PyObject *rhs, int oparg,
                                   _PyBinaryOpSpecializationDescr **descr)
 {
-    size_t n = sizeof(binaryop_extend_descrs)/sizeof(_PyBinaryOpSpecializationDescr);
-    for (size_t i = 0; i < n; i++) {
-        _PyBinaryOpSpecializationDescr *d = &binaryop_extend_descrs[i];
+    typedef _PyBinaryOpSpecializationDescr descr_type;
+    size_t size = sizeof(binaryop_extend_builtins)/sizeof(descr_type);
+    for (size_t i = 0; i < size; i++) {
+        descr_type *d = (descr_type *)&binaryop_extend_builtins[i];
+        assert(d != NULL);
+        assert(d->guard != NULL);
         if (d->oparg == oparg && d->guard(lhs, rhs)) {
             *descr = d;
             return 1;
         }
     }
+
+    if (Py_TYPE(lhs)->tp_binop_specialize != NULL) {
+        int ret = Py_TYPE(lhs)->tp_binop_specialize(lhs, rhs, oparg, descr);
+        if (ret < 0) {
+            return -1;
+        }
+        if (ret == 1) {
+            if (*descr == NULL) {
+                PyErr_Format(
path: Python/specialize.c
user: login: picnixz
body: Maybe chain exceptions if possible here? it could be helpful for extensions to know that they set an exception but returned 1.
created_at: 2025-05-05T09:36:35Z
author_association: MEMBER
diff_hunk: @@ -348,6 +348,8 @@ typedef int (*objobjproc)(PyObject *, PyObject *);
 typedef int (*visitproc)(PyObject *, void *);
 typedef int (*traverseproc)(PyObject *, visitproc, void *);
 
+struct _PyBinaryOpSpecializationDescr;
+typedef int (*binopspecfunc)(PyObject *lhs, PyObject *rhs, int oparg, struct _PyBinaryOpSpecializationDescr* descr);
path: Include/object.h
user: login: iritkatriel
body: which other?
created_at: 2025-05-05T11:16:31Z
author_association: MEMBER
diff_hunk: @@ -2560,14 +2560,35 @@ static int
 binary_op_extended_specialization(PyObject *lhs, PyObject *rhs, int oparg,
                                   _PyBinaryOpSpecializationDescr **descr)
 {
-    size_t n = sizeof(binaryop_extend_descrs)/sizeof(_PyBinaryOpSpecializationDescr);
-    for (size_t i = 0; i < n; i++) {
-        _PyBinaryOpSpecializationDescr *d = &binaryop_extend_descrs[i];
+    typedef _PyBinaryOpSpecializationDescr descr_type;
+    size_t size = sizeof(binaryop_extend_builtins)/sizeof(descr_type);
path: Python/specialize.c
user: login: iritkatriel
body: Not aware of one.
created_at: 2025-05-05T11:19:39Z
author_association: MEMBER
diff_hunk: @@ -2560,14 +2560,35 @@ static int
 binary_op_extended_specialization(PyObject *lhs, PyObject *rhs, int oparg,
                                   _PyBinaryOpSpecializationDescr **descr)
 {
-    size_t n = sizeof(binaryop_extend_descrs)/sizeof(_PyBinaryOpSpecializationDescr);
-    for (size_t i = 0; i < n; i++) {
-        _PyBinaryOpSpecializationDescr *d = &binaryop_extend_descrs[i];
+    typedef _PyBinaryOpSpecializationDescr descr_type;
+    size_t size = sizeof(binaryop_extend_builtins)/sizeof(descr_type);
+    for (size_t i = 0; i < size; i++) {
+        descr_type *d = (descr_type *)&binaryop_extend_builtins[i];
+        assert(d != NULL);
+        assert(d->guard != NULL);
         if (d->oparg == oparg && d->guard(lhs, rhs)) {
             *descr = d;
             return 1;
         }
     }
+
+    if (Py_TYPE(lhs)->tp_binop_specialize != NULL) {
+        int ret = Py_TYPE(lhs)->tp_binop_specialize(lhs, rhs, oparg, descr);
+        if (ret < 0) {
+            return -1;
+        }
+        if (ret == 1) {
+            if (*descr == NULL) {
+                PyErr_Format(
path: Python/specialize.c
user: login: iritkatriel
body: It's a bit awkward to do, and I don't think it's that useful. If the guard/action functions are buggy then the context in which specialisation was kicked off is unlikely to be relevant.
created_at: 2025-05-05T11:25:48Z
author_association: MEMBER
diff_hunk: @@ -348,6 +348,8 @@ typedef int (*objobjproc)(PyObject *, PyObject *);
 typedef int (*visitproc)(PyObject *, void *);
 typedef int (*traverseproc)(PyObject *, visitproc, void *);
 
+struct _PyBinaryOpSpecializationDescr;
+typedef int (*binopspecfunc)(PyObject *lhs, PyObject *rhs, int oparg, struct _PyBinaryOpSpecializationDescr* descr);
path: Include/object.h
user: login: picnixz
body: I think it's the same signature as `binop_specialize_func` right in Include/cpython/object.h?

created_at: 2025-05-05T11:37:07Z
author_association: MEMBER
diff_hunk: @@ -2560,14 +2560,36 @@ static int
 binary_op_extended_specialization(PyObject *lhs, PyObject *rhs, int oparg,
                                   _PyBinaryOpSpecializationDescr **descr)
 {
-    size_t n = sizeof(binaryop_extend_descrs)/sizeof(_PyBinaryOpSpecializationDescr);
-    for (size_t i = 0; i < n; i++) {
-        _PyBinaryOpSpecializationDescr *d = &binaryop_extend_descrs[i];
+    typedef _PyBinaryOpSpecializationDescr descr_type;
+    size_t size = sizeof(binaryop_extend_builtins)/sizeof(descr_type);
+    for (size_t i = 0; i < size; i++) {
+        descr_type *d = (descr_type *)&binaryop_extend_builtins[i];
+        assert(d != NULL);
+        assert(d->guard != NULL);
         if (d->oparg == oparg && d->guard(lhs, rhs)) {
             *descr = d;
             return 1;
         }
     }
+
+    PyTypeObject *lhs_type = Py_TYPE(lhs);
+    if (lhs_type->tp_binop_specialize != NULL) {
+        int ret = lhs_type->tp_binop_specialize(lhs, rhs, oparg, descr);
+        if (ret < 0) {
+            return -1;
+        }
+        if (ret == 1) {
+            if (*descr == NULL) {
+                PyErr_Format(
+                    PyExc_ValueError,
+                    "tp_binop_specialize of '%T' returned 1 with *descr == NULL",
path: Python/specialize.c
user: login: picnixz
body: I'm sorry: My suggestion was to use %T but this means using "lhs" and not "Py_TYPE(lhs)->tp_name" (%T is essentially doing the tpname lookup for you) (this is PEP-737)
created_at: 2025-05-05T11:39:41Z
author_association: MEMBER
diff_hunk: @@ -2995,6 +3003,70 @@ static PyType_Spec array_spec = {
     .slots = array_slots,
 };
 
+static inline int
+array_subscr_guard(PyObject *lhs, PyObject *rhs)
path: Modules/arraymodule.c
user: login: eendebakpt
body: What about specializing also on the rhs? (on int or slices)

It would make the code more complex, as the action would have to be split or duplicated. But since performance is the reason for adding this mechanism, maybe the extra specialization is needed.
created_at: 2025-05-05T11:41:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -348,6 +348,8 @@ typedef int (*objobjproc)(PyObject *, PyObject *);
 typedef int (*visitproc)(PyObject *, void *);
 typedef int (*traverseproc)(PyObject *, visitproc, void *);
 
+struct _PyBinaryOpSpecializationDescr;
+typedef int (*binopspecfunc)(PyObject *lhs, PyObject *rhs, int oparg, struct _PyBinaryOpSpecializationDescr* descr);
path: Include/object.h
user: login: iritkatriel
body: Ah, thanks. This one is not used, I'll delete it.
created_at: 2025-05-05T11:46:09Z
author_association: MEMBER
diff_hunk: @@ -2560,14 +2560,35 @@ static int
 binary_op_extended_specialization(PyObject *lhs, PyObject *rhs, int oparg,
                                   _PyBinaryOpSpecializationDescr **descr)
 {
-    size_t n = sizeof(binaryop_extend_descrs)/sizeof(_PyBinaryOpSpecializationDescr);
-    for (size_t i = 0; i < n; i++) {
-        _PyBinaryOpSpecializationDescr *d = &binaryop_extend_descrs[i];
+    typedef _PyBinaryOpSpecializationDescr descr_type;
+    size_t size = sizeof(binaryop_extend_builtins)/sizeof(descr_type);
path: Python/specialize.c
user: login: picnixz
body: Ah it was `Py_ARRAY_LENGTH` not `Py_ARRAY_SIZE` (it needs at least one element in the array)
created_at: 2025-05-05T11:50:20Z
author_association: MEMBER
diff_hunk: @@ -2995,6 +3003,70 @@ static PyType_Spec array_spec = {
     .slots = array_slots,
 };
 
+static inline int
+array_subscr_guard(PyObject *lhs, PyObject *rhs)
path: Modules/arraymodule.c
user: login: iritkatriel
body: Indeed, we should do that. At the moment I'm just adding the plumbing and implementing a very basic specialisation.
created_at: 2025-05-05T11:51:12Z
author_association: MEMBER
diff_hunk: @@ -233,6 +238,9 @@ struct _typeobject {
     /* bitset of which type-watchers care about this type */
     unsigned char tp_watched;
 
+    /* callback that may specialize BINARY_OP */
+    binop_specialize_func tp_binop_specialize;
path: Include/cpython/object.h
user: login: iritkatriel
body: I can put it in `PySequenceMethods` and `PyMappingMethods`, but then it appears twice. Is there another way?
created_at: 2025-05-05T12:27:22Z
author_association: MEMBER
diff_hunk: @@ -233,6 +238,9 @@ struct _typeobject {
     /* bitset of which type-watchers care about this type */
     unsigned char tp_watched;
 
+    /* callback that may specialize BINARY_OP */
+    binop_specialize_func tp_binop_specialize;
path: Include/cpython/object.h
user: login: markshannon
body: That doesn't help. I would prefer if it wasn't in a struct in a public header, but I don't see any other obvious place to put the data.
Maybe add a comment to the effect that this isn't part of the public API.
created_at: 2025-05-05T12:54:48Z
author_association: MEMBER
diff_hunk: @@ -143,6 +143,11 @@ typedef struct {
  * backwards-compatibility */
 typedef Py_ssize_t printfunc;
 
+/* Specialize a binary by setting the descriptor pointer */
path: Include/cpython/object.h
user: login: mdboom
body: ```suggestion
/* Specialize a binary op by setting the descriptor pointer */
```
created_at: 2025-05-05T13:22:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,2 @@
+Add option for extension modules to specialised ``BINARY_OP`` instructions.
path: Misc/NEWS.d/next/Core_and_Builtins/2025-05-04-19-32-47.gh-issue-133395.VhWWEP.rst
user: login: mdboom
body: ```suggestion
Add option for extension modules to specialize ``BINARY_OP`` instructions.
```
created_at: 2025-05-05T13:23:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2560,14 +2560,36 @@ static int
 binary_op_extended_specialization(PyObject *lhs, PyObject *rhs, int oparg,
                                   _PyBinaryOpSpecializationDescr **descr)
 {
-    size_t n = sizeof(binaryop_extend_descrs)/sizeof(_PyBinaryOpSpecializationDescr);
-    for (size_t i = 0; i < n; i++) {
-        _PyBinaryOpSpecializationDescr *d = &binaryop_extend_descrs[i];
+    typedef _PyBinaryOpSpecializationDescr descr_type;
+    size_t size = Py_ARRAY_LENGTH(binaryop_extend_builtins);
+    for (size_t i = 0; i < size; i++) {
+        descr_type *d = (descr_type *)&binaryop_extend_builtins[i];
+        assert(d != NULL);
+        assert(d->guard != NULL);
         if (d->oparg == oparg && d->guard(lhs, rhs)) {
             *descr = d;
             return 1;
         }
     }
+
+    PyTypeObject *lhs_type = Py_TYPE(lhs);
path: Python/specialize.c
user: login: mdboom
body: As I think we had discussed this offline, this logic is a bit different from operator overloading like `__add__`, since there the `rhs` can also come into play.  For this first "proof of concept" with `BINARY_SUBSCR`/`__getitem__`, it doesn't matter, but maybe leave a comment here for the future where we start to add more specializations?  It's not critical, since the difference (I think) would just mean a lost potential optimization, not a different in behaviour.
created_at: 2025-05-05T13:29:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1147,26 +1147,7 @@
             break;
         }
 
-        case _GUARD_BINARY_OP_EXTEND: {
-            _PyStackRef right;
-            _PyStackRef left;
-            right = stack_pointer[-1];
-            left = stack_pointer[-2];
-            PyObject *descr = (PyObject *)CURRENT_OPERAND0();
-            PyObject *left_o = PyStackRef_AsPyObjectBorrow(left);
-            PyObject *right_o = PyStackRef_AsPyObjectBorrow(right);
-            _PyBinaryOpSpecializationDescr *d = (_PyBinaryOpSpecializationDescr*)descr;
-            assert(INLINE_CACHE_ENTRIES_BINARY_OP == 5);
-            assert(d && d->guard);
-            _PyFrame_SetStackPointer(frame, stack_pointer);
-            int res = d->guard(left_o, right_o);
-            stack_pointer = _PyFrame_GetStackPointer(frame);
-            if (!res) {
-                UOP_STAT_INC(uopcode, miss);
-                JUMP_TO_JUMP_TARGET();
-            }
-            break;
-        }
+        /* _GUARD_BINARY_OP_EXTEND is not a viable micro-op for tier 2 because it uses the 'this_instr' variable */
path: Python/executor_cases.c.h
user: login: brandtbucher
body: FYI, this change made it so that any traces containing `BINARY_OP_EXTEND` can't be JIT-compiled. Is there a way to rewrite this to be tier-2 friendly? Mutating inline caches doesn't work there.
created_at: 2025-05-06T05:10:02Z
author_association: MEMBER
diff_hunk: @@ -1147,26 +1147,7 @@
             break;
         }
 
-        case _GUARD_BINARY_OP_EXTEND: {
-            _PyStackRef right;
-            _PyStackRef left;
-            right = stack_pointer[-1];
-            left = stack_pointer[-2];
-            PyObject *descr = (PyObject *)CURRENT_OPERAND0();
-            PyObject *left_o = PyStackRef_AsPyObjectBorrow(left);
-            PyObject *right_o = PyStackRef_AsPyObjectBorrow(right);
-            _PyBinaryOpSpecializationDescr *d = (_PyBinaryOpSpecializationDescr*)descr;
-            assert(INLINE_CACHE_ENTRIES_BINARY_OP == 5);
-            assert(d && d->guard);
-            _PyFrame_SetStackPointer(frame, stack_pointer);
-            int res = d->guard(left_o, right_o);
-            stack_pointer = _PyFrame_GetStackPointer(frame);
-            if (!res) {
-                UOP_STAT_INC(uopcode, miss);
-                JUMP_TO_JUMP_TARGET();
-            }
-            break;
-        }
+        /* _GUARD_BINARY_OP_EXTEND is not a viable micro-op for tier 2 because it uses the 'this_instr' variable */
path: Python/executor_cases.c.h
user: login: iritkatriel
body: We probably don't have to NULL the cache, just change the opcode to BINARY_OP and hope nobody tries to use the pointer in the cache anymore. Will make it harder to debug if they do.
created_at: 2025-05-06T07:54:06Z
author_association: MEMBER

PR comments:
user: login: iritkatriel
created_at: 2025-05-05T11:12:19Z
body: > Is the `_PyBinopSpecializationDescr` supposed to be extensible?

I assumed it's extensible and the extension manages it. The alternative is that it has a `void*` field that the extension sets and manages. The advantage of letting extensions allocate is that if there is no dynamic information, you don't need to allocate a new one for each instruction that uses the specialisation. 
author_association: MEMBER
user: login: eendebakpt
created_at: 2025-05-05T11:37:30Z
body: Are there any microbenchmarks that show this improves performance?
author_association: CONTRIBUTOR
user: login: brandtbucher
created_at: 2025-05-06T05:43:52Z
body: Also, based on the benchmarking results that were posted, it looks like the `coverage`, `sqlalchemy_declarative`, and `sqlalchemy_imperative` benchmarks crashed on this branch but succeeded on the base commit. Were these fixed before merging?
author_association: MEMBER
user: login: nascheme
created_at: 2025-05-06T05:54:04Z
body: This seems to cause the refleaks unit test to fail for the test_datetime module. The leak seems to be related to the `ZoneInfoTest` class, which uses the `array.array` type.
author_association: MEMBER
user: login: iritkatriel
created_at: 2025-05-06T06:04:55Z
body: The crash should have been fixed. I'll check the leak.

Re tier 2 - maybe there's a way but we can just deopt them when jitting if not.
author_association: MEMBER
user: login: vstinner
created_at: 2025-05-06T07:08:32Z
body: > This seems to cause the refleaks unit test to fail for the test_datetime module. The leak seems to be related to the ZoneInfoTest class, which uses the array.array type.

The follow code is enough to reprodue the issue:

```py
import array
import gc

def func():
    a=array.array('B', b'hello world')
    # call array_binop_specialize() 3 times, once per loop
    print("specialize")
    for _ in range(10): a[1]
    print("specialize")
    for _ in range(10): a[1]
    print("specialize")
    for _ in range(10): a[1]

func()

# array_subscr_free() is not called
del func
gc.collect()
```

The specialization calls PyMem_Malloc() in array_binop_specialize(), but nothing calls PyMem_Free(): array_subscr_free() is not called.
author_association: MEMBER
user: login: Eclips4
created_at: 2025-05-06T07:31:41Z
body: It seems that `array_subscr_guard` behaves incorrectly. It never returns `0` (at least I can't make it return `0`), which is needed to trigger `array_subscr_free`.
author_association: MEMBER
user: login: iritkatriel
created_at: 2025-05-06T07:35:56Z
body: > It seems that `array_subscr_guard` behaves incorrectly. It never returns `0` (at least I can't make it return `0`), which is needed to trigger `array_subscr_free`.

I don't think that's the issue. Free needs to happen when the code object is freed, and currently it's not.
author_association: MEMBER
user: login: iritkatriel
created_at: 2025-05-06T08:52:08Z
body: I'm going to revert this so it doesn't delay the release today. 
https://github.com/python/cpython/pull/133498

It's not terribly important for this to be in 3.14.
author_association: MEMBER
user: login: brandtbucher
created_at: 2025-05-06T17:00:15Z
body: @iritkatriel I just ran JIT benchmarks on the head of this branch. Those three benchmarks are still broken, and it makes the JIT 1% slower (with individual benchmarks slowing down up to 25%, one benchmark using 8% more memory):

https://github.com/faster-cpython/benchmarking-public/blob/main/results/bm-20250505-3.14.0a7%2B-454bfc5-JIT/README.md#vs-base-1

So we should make sure to benchmark the JIT when this lands again, or find a new approach (I don't *love* having strong references in the inline caches, even just pointers to owned memory).
author_association: MEMBER