diff --git a/Include/internal/pycore_long.h b/Include/internal/pycore_long.h index fb5622c99f7a135..ee3f4e4b27b1a4d 100644 --- a/Include/internal/pycore_long.h +++ b/Include/internal/pycore_long.h @@ -116,6 +116,8 @@ PyAPI_DATA(PyObject*) _PyLong_Lshift(PyObject *, int64_t); PyAPI_FUNC(_PyStackRef) _PyCompactLong_Add(PyLongObject *left, PyLongObject *right); PyAPI_FUNC(_PyStackRef) _PyCompactLong_Multiply(PyLongObject *left, PyLongObject *right); PyAPI_FUNC(_PyStackRef) _PyCompactLong_Subtract(PyLongObject *left, PyLongObject *right); +PyAPI_FUNC(PyObject*) _PyLong_Add(PyLongObject *left, PyLongObject *right); +PyAPI_FUNC(PyObject*) _PyLong_Subtract(PyLongObject *left, PyLongObject *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..8ed5b143c37e40e 100644 --- a/Objects/longobject.c +++ b/Objects/longobject.c @@ -3923,6 +3923,18 @@ long_sub_method(PyObject *a, PyObject *b) return (PyObject*)long_sub((PyLongObject*)a, (PyLongObject*)b); } +PyObject * +_PyLong_Add(PyLongObject *a, PyLongObject *b) +{ + return (PyObject *)long_add(a, b); +} + +PyObject * +_PyLong_Subtract(PyLongObject *a, PyLongObject *b) +{ + return (PyObject *)long_sub(a, 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..709735c15b5d5b9 100644 --- a/Python/specialize.c +++ b/Python/specialize.c @@ -2157,6 +2157,21 @@ SEQ_INT_MULTIPLY_ACTION(tuple_int_multiply, _PyTuple_Repeat, lhs, rhs) SEQ_INT_MULTIPLY_ACTION(int_tuple_multiply, _PyTuple_Repeat, rhs, lhs) #undef SEQ_INT_MULTIPLY_ACTION +/* 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. */ +static PyObject * +longs_add(PyObject *lhs, PyObject *rhs) +{ + return _PyLong_Add((PyLongObject *)lhs, (PyLongObject *)rhs); +} + +static PyObject * +longs_subtract(PyObject *lhs, PyObject *rhs) +{ + return _PyLong_Subtract((PyLongObject *)lhs, (PyLongObject *)rhs); +} + static int compactlongs_guard(PyObject *lhs, PyObject *rhs) { @@ -2256,6 +2271,14 @@ 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 operands that are not both compact. + 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, longs_add, &PyLong_Type, 1, &PyLong_Type, &PyLong_Type}, + {NB_SUBTRACT, NULL, longs_subtract, &PyLong_Type, 1, &PyLong_Type, &PyLong_Type}, + {NB_INPLACE_ADD, NULL, longs_add, &PyLong_Type, 1, &PyLong_Type, &PyLong_Type}, + {NB_INPLACE_SUBTRACT, NULL, longs_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},