From c42a7a866dca12336814ae6f652ce24468b34039 Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Mon, 6 Apr 2026 00:34:11 +0200 Subject: [PATCH] gh-100239: Propagate type info through _BINARY_OP_EXTEND in tier 2 Adds two new fields to _PyBinaryOpSpecializationDescr: - result_type: the static type of the result (or NULL if unknown). - result_unique: nonzero iff `action` always returns a freshly allocated object (not aliased to either operand). The tier 2 optimizer now narrows the result symbol's type via sym_new_type(d->result_type) and wraps it in PyJitRef_MakeUnique when d->result_unique is set. This lets downstream ops elide their operand-type guards and pick inplace variants. For example, (2 + x) * y with x, y floats now compiles to _BINARY_OP_MULTIPLY_FLOAT_INPLACE with _GUARD_NOS_FLOAT eliminated. All existing descriptors populate both fields: long-long bitwise ops produce unique PyLong results, and float/long mixed arithmetic produces unique PyFloat results. A test verifies the inplace-multiply case end-to-end. --- Include/internal/pycore_code.h | 7 +++++ Lib/test/test_capi/test_opt.py | 23 +++++++++++++++ ...04-06-00-00-00.gh-issue-100239.binopxt.rst | 3 ++ Python/optimizer_bytecodes.c | 12 ++++++-- Python/optimizer_cases.c.h | 12 ++++++-- Python/specialize.c | 28 +++++++++---------- 6 files changed, 67 insertions(+), 18 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-04-06-00-00-00.gh-issue-100239.binopxt.rst diff --git a/Include/internal/pycore_code.h b/Include/internal/pycore_code.h index 376e68a4c8773c..fe8d0a54f2af1a 100644 --- a/Include/internal/pycore_code.h +++ b/Include/internal/pycore_code.h @@ -496,6 +496,13 @@ typedef struct { int oparg; binaryopguardfunc guard; binaryopactionfunc action; + /* Static type of the result, or NULL if unknown. Used by the tier 2 + optimizer to propagate type information through _BINARY_OP_EXTEND. */ + PyTypeObject *result_type; + /* Nonzero iff `action` always returns a freshly allocated object (not + aliased to either operand). Used by the tier 2 optimizer to enable + inplace follow-up ops. */ + int result_unique; } _PyBinaryOpSpecializationDescr; /* Comparison bit masks. */ diff --git a/Lib/test/test_capi/test_opt.py b/Lib/test/test_capi/test_opt.py index 56f90194b480a1..b31c9f68d01bec 100644 --- a/Lib/test/test_capi/test_opt.py +++ b/Lib/test/test_capi/test_opt.py @@ -3813,6 +3813,29 @@ def f(n): self.assertIn("_UNPACK_SEQUENCE_TWO_TUPLE", uops) self.assertNotIn("_GUARD_TOS_TUPLE", uops) + def test_binary_op_extend_float_result_enables_inplace_multiply(self): + # (2 + x) * y with x, y floats: `2 + x` goes through _BINARY_OP_EXTEND + # (int + float). The result_type/result_unique info should let the + # subsequent float multiply use the inplace variant. + def testfunc(n): + x = 3.5 + y = 2.0 + res = 0.0 + for _ in range(n): + res = (2 + x) * y + return res + + res, ex = self._run_with_optimizer(testfunc, TIER2_THRESHOLD) + self.assertEqual(res, 11.0) + self.assertIsNotNone(ex) + uops = get_opnames(ex) + self.assertIn("_BINARY_OP_EXTEND", uops) + self.assertIn("_BINARY_OP_MULTIPLY_FLOAT_INPLACE", uops) + self.assertNotIn("_BINARY_OP_MULTIPLY_FLOAT", uops) + # NOS guard on the multiply is eliminated because _BINARY_OP_EXTEND + # propagates PyFloat_Type. + self.assertNotIn("_GUARD_NOS_FLOAT", uops) + def test_unary_invert_long_type(self): def testfunc(n): for _ in range(n): diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-04-06-00-00-00.gh-issue-100239.binopxt.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-04-06-00-00-00.gh-issue-100239.binopxt.rst new file mode 100644 index 00000000000000..9eccef3ef9d342 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-04-06-00-00-00.gh-issue-100239.binopxt.rst @@ -0,0 +1,3 @@ +Propagate result type and uniqueness information through +``_BINARY_OP_EXTEND`` in the tier 2 optimizer, enabling elimination of +downstream type guards and selection of inplace float operations. diff --git a/Python/optimizer_bytecodes.c b/Python/optimizer_bytecodes.c index b8148ef57ede0c..58b50707e55cee 100644 --- a/Python/optimizer_bytecodes.c +++ b/Python/optimizer_bytecodes.c @@ -410,8 +410,16 @@ dummy_func(void) { } op(_BINARY_OP_EXTEND, (descr/4, left, right -- res, l, r)) { - (void)descr; - res = sym_new_not_null(ctx); + _PyBinaryOpSpecializationDescr *d = (_PyBinaryOpSpecializationDescr *)descr; + if (d != NULL && d->result_type != NULL) { + res = sym_new_type(ctx, d->result_type); + if (d->result_unique) { + res = PyJitRef_MakeUnique(res); + } + } + else { + res = sym_new_not_null(ctx); + } l = left; r = right; } diff --git a/Python/optimizer_cases.c.h b/Python/optimizer_cases.c.h index a15b5ae1d13d3b..891887301119d7 100644 --- a/Python/optimizer_cases.c.h +++ b/Python/optimizer_cases.c.h @@ -1168,8 +1168,16 @@ right = stack_pointer[-1]; left = stack_pointer[-2]; PyObject *descr = (PyObject *)this_instr->operand0; - (void)descr; - res = sym_new_not_null(ctx); + _PyBinaryOpSpecializationDescr *d = (_PyBinaryOpSpecializationDescr *)descr; + if (d != NULL && d->result_type != NULL) { + res = sym_new_type(ctx, d->result_type); + if (d->result_unique) { + res = PyJitRef_MakeUnique(res); + } + } + else { + res = sym_new_not_null(ctx); + } l = left; r = right; CHECK_STACK_BOUNDS(1); diff --git a/Python/specialize.c b/Python/specialize.c index 09ec25767a4c3f..0fe225dcbb6b5f 100644 --- a/Python/specialize.c +++ b/Python/specialize.c @@ -2195,24 +2195,24 @@ LONG_FLOAT_ACTION(compactlong_float_true_div, /) static _PyBinaryOpSpecializationDescr binaryop_extend_descrs[] = { /* long-long arithmetic */ - {NB_OR, compactlongs_guard, compactlongs_or}, - {NB_AND, compactlongs_guard, compactlongs_and}, - {NB_XOR, compactlongs_guard, compactlongs_xor}, - {NB_INPLACE_OR, compactlongs_guard, compactlongs_or}, - {NB_INPLACE_AND, compactlongs_guard, compactlongs_and}, - {NB_INPLACE_XOR, compactlongs_guard, compactlongs_xor}, + {NB_OR, compactlongs_guard, compactlongs_or, &PyLong_Type, 1}, + {NB_AND, compactlongs_guard, compactlongs_and, &PyLong_Type, 1}, + {NB_XOR, compactlongs_guard, compactlongs_xor, &PyLong_Type, 1}, + {NB_INPLACE_OR, compactlongs_guard, compactlongs_or, &PyLong_Type, 1}, + {NB_INPLACE_AND, compactlongs_guard, compactlongs_and, &PyLong_Type, 1}, + {NB_INPLACE_XOR, compactlongs_guard, compactlongs_xor, &PyLong_Type, 1}, /* float-long arithemetic */ - {NB_ADD, float_compactlong_guard, float_compactlong_add}, - {NB_SUBTRACT, float_compactlong_guard, float_compactlong_subtract}, - {NB_TRUE_DIVIDE, nonzero_float_compactlong_guard, float_compactlong_true_div}, - {NB_MULTIPLY, float_compactlong_guard, float_compactlong_multiply}, + {NB_ADD, float_compactlong_guard, float_compactlong_add, &PyFloat_Type, 1}, + {NB_SUBTRACT, float_compactlong_guard, float_compactlong_subtract, &PyFloat_Type, 1}, + {NB_TRUE_DIVIDE, nonzero_float_compactlong_guard, float_compactlong_true_div, &PyFloat_Type, 1}, + {NB_MULTIPLY, float_compactlong_guard, float_compactlong_multiply, &PyFloat_Type, 1}, /* float-float arithmetic */ - {NB_ADD, compactlong_float_guard, compactlong_float_add}, - {NB_SUBTRACT, compactlong_float_guard, compactlong_float_subtract}, - {NB_TRUE_DIVIDE, nonzero_compactlong_float_guard, compactlong_float_true_div}, - {NB_MULTIPLY, compactlong_float_guard, compactlong_float_multiply}, + {NB_ADD, compactlong_float_guard, compactlong_float_add, &PyFloat_Type, 1}, + {NB_SUBTRACT, compactlong_float_guard, compactlong_float_subtract, &PyFloat_Type, 1}, + {NB_TRUE_DIVIDE, nonzero_compactlong_float_guard, compactlong_float_true_div, &PyFloat_Type, 1}, + {NB_MULTIPLY, compactlong_float_guard, compactlong_float_multiply, &PyFloat_Type, 1}, }; static int