diff --git a/Include/internal/pycore_long.h b/Include/internal/pycore_long.h index fb5622c99f7a135..bbaf59aab46d638 100644 --- a/Include/internal/pycore_long.h +++ b/Include/internal/pycore_long.h @@ -117,6 +117,12 @@ PyAPI_FUNC(_PyStackRef) _PyCompactLong_Add(PyLongObject *left, PyLongObject *rig PyAPI_FUNC(_PyStackRef) _PyCompactLong_Multiply(PyLongObject *left, PyLongObject *right); PyAPI_FUNC(_PyStackRef) _PyCompactLong_Subtract(PyLongObject *left, PyLongObject *right); +// Add or subtract two int objects, calling the long implementation directly +// instead of going through the number protocol. Used by the BINARY_OP_EXTEND +// specialization for operands that are not both compact. +extern PyObject* _PyLong_Add(PyObject *left, PyObject *right); +extern PyObject* _PyLong_Subtract(PyObject *left, PyObject *right); + // Export for 'binascii' shared extension. PyAPI_DATA(unsigned char) _PyLong_DigitValue[256]; diff --git a/Lib/test/test_capi/test_opt.py b/Lib/test/test_capi/test_opt.py index 88077aacecbf5b1..8ea34db410ebdbc 100644 --- a/Lib/test/test_capi/test_opt.py +++ b/Lib/test/test_capi/test_opt.py @@ -4887,6 +4887,47 @@ def testfunc(n): self.assertIn("_UNPACK_SEQUENCE_TUPLE", uops) self.assertNotIn("_GUARD_TOS_TUPLE", uops) + def test_binary_op_extend_wide_int_subtract(self): + # Non-compact int - int is specialized via BINARY_OP_EXTEND. The + # result is known to be an int, so the second subtraction only needs + # to guard its rhs. + def testfunc(n): + a = 10**20 + b = 10**19 + c = 10**18 + res = 0 + for _ in range(n): + res = (a - b) - c + return res + + res, ex = self._run_with_optimizer(testfunc, TIER2_THRESHOLD) + self.assertEqual(res, 10**20 - 10**19 - 10**18) + self.assertIsNotNone(ex) + uops = get_opnames(ex) + self.assertEqual(uops.count("_BINARY_OP_EXTEND"), 2) + self.assertEqual(uops.count("_GUARD_BINARY_OP_EXTEND"), 1) + self.assertIn("_GUARD_BINARY_OP_EXTEND_RHS", uops) + self.assertNotIn("_BINARY_OP_SUBTRACT_INT", uops) + + def test_compact_int_ops_see_non_compact_operands(self): + # Trace compiled for compact ints, then re-entered with operands whose + # difference is not compact. The follow-up int op must not treat the + # result as compact. + def testfunc(args): + a, b, c, n = args + res = 0 + for _ in range(n): + res = (a - b) - c + return res + + res, ex = self._run_with_optimizer(testfunc, (5, 2, 1000, TIER2_THRESHOLD)) + self.assertEqual(res, -997) + self.assertIsNotNone(ex) + self.assertIn("_BINARY_OP_SUBTRACT_INT", get_opnames(ex)) + a, b = 2**60 + 2**59, 2**60 + res = testfunc((a, b, 1000, TIER2_THRESHOLD)) + self.assertEqual(res, 2**59 - 1000) + def test_binary_op_extend_guard_elimination(self): # When both operands have known types (e.g., from a prior # _BINARY_OP_EXTEND result), the _GUARD_BINARY_OP_EXTEND diff --git a/Lib/test/test_opcache.py b/Lib/test/test_opcache.py index 60879e2774e7077..4a42a72080b9813 100644 --- a/Lib/test/test_opcache.py +++ b/Lib/test/test_opcache.py @@ -1536,6 +1536,39 @@ def binary_op_bitwise_extend(): self.assert_specialized(binary_op_bitwise_extend, "BINARY_OP_EXTEND") self.assert_no_opcode(binary_op_bitwise_extend, "BINARY_OP") + def binary_op_wide_int_extend(): + for _ in range(100): + a, b = 10**20, 10**19 + x = a + b + self.assertEqual(x, 11 * 10**19) + y = a - b + self.assertEqual(y, 9 * 10**19) + a += b + self.assertEqual(a, 11 * 10**19) + a -= b + self.assertEqual(a, 10**20) + + binary_op_wide_int_extend() + self.assert_specialized(binary_op_wide_int_extend, "BINARY_OP_EXTEND") + self.assert_no_opcode(binary_op_wide_int_extend, "BINARY_OP") + self.assert_no_opcode(binary_op_wide_int_extend, "BINARY_OP_ADD_INT") + self.assert_no_opcode(binary_op_wide_int_extend, "BINARY_OP_SUBTRACT_INT") + + def wide_int_extend_mixed(a, b): + return a - b, a + b + + # Specialized on wide operands; compact operands and bool still + # produce correct results (bool is not an exact int and deopts). + for _ in range(100): + self.assertEqual(wide_int_extend_mixed(10**20, 1), + (10**20 - 1, 10**20 + 1)) + self.assert_specialized(wide_int_extend_mixed, "BINARY_OP_EXTEND") + self.assertEqual(wide_int_extend_mixed(5, 7), (-2, 12)) + self.assertEqual(wide_int_extend_mixed(True, 10**20), + (1 - 10**20, 1 + 10**20)) + self.assertEqual(wide_int_extend_mixed(-10**20, 10**20), + (-2 * 10**20, 0)) + @cpython_only @requires_specialization def test_load_super_attr(self): diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-18-12-00-00.gh-issue-151289.wIdEx7.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-18-12-00-00.gh-issue-151289.wIdEx7.rst new file mode 100644 index 000000000000000..150bcb709adda19 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-18-12-00-00.gh-issue-151289.wIdEx7.rst @@ -0,0 +1,3 @@ +Specialize :class:`int` addition and subtraction where an operand is not a +compact integer, using ``BINARY_OP_EXTEND``. This skips the generic binary +operator dispatch for these cases. diff --git a/Objects/longobject.c b/Objects/longobject.c index 9577c8ef4d6ed08..e71242e2459fc5d 100644 --- a/Objects/longobject.c +++ b/Objects/longobject.c @@ -3923,6 +3923,20 @@ long_sub_method(PyObject *a, PyObject *b) return (PyObject*)long_sub((PyLongObject*)a, (PyLongObject*)b); } +PyObject * +_PyLong_Add(PyObject *a, PyObject *b) +{ + assert(PyLong_Check(a) && PyLong_Check(b)); + return (PyObject *)long_add((PyLongObject *)a, (PyLongObject *)b); +} + +PyObject * +_PyLong_Subtract(PyObject *a, PyObject *b) +{ + assert(PyLong_Check(a) && PyLong_Check(b)); + return (PyObject *)long_sub((PyLongObject *)a, (PyLongObject *)b); +} + /* Grade school multiplication, ignoring the signs. * Returns the absolute value of the product, or NULL if error. diff --git a/Python/specialize.c b/Python/specialize.c index f658df3bc5d2228..e5813b42246d833 100644 --- a/Python/specialize.c +++ b/Python/specialize.c @@ -2256,6 +2256,16 @@ static _PyBinaryOpSpecializationDescr binaryop_extend_descrs[] = { {NB_INPLACE_AND, compactlongs_guard, compactlongs_and, &PyLong_Type, 1, NULL, NULL}, {NB_INPLACE_XOR, compactlongs_guard, compactlongs_xor, &PyLong_Type, 1, NULL, NULL}, + /* long-long add/subtract for exact ints that are not both compact (those + take BINARY_OP_ADD_INT and friends). Calling the long implementation + directly skips the generic binary_op1 slot dispatch. The guard is a + pure type check: the result may be non-compact, so it is typed as int + without compactness and follow-up int guards stay. */ + {NB_ADD, NULL, _PyLong_Add, &PyLong_Type, 1, &PyLong_Type, &PyLong_Type}, + {NB_SUBTRACT, NULL, _PyLong_Subtract, &PyLong_Type, 1, &PyLong_Type, &PyLong_Type}, + {NB_INPLACE_ADD, NULL, _PyLong_Add, &PyLong_Type, 1, &PyLong_Type, &PyLong_Type}, + {NB_INPLACE_SUBTRACT, NULL, _PyLong_Subtract, &PyLong_Type, 1, &PyLong_Type, &PyLong_Type}, + /* float-long arithmetic: guards also check NaN and compactness. */ {NB_ADD, float_compactlong_guard, float_compactlong_add, &PyFloat_Type, 1, NULL, NULL}, {NB_SUBTRACT, float_compactlong_guard, float_compactlong_subtract, &PyFloat_Type, 1, NULL, NULL},