diff --git a/Include/internal/pycore_bytesobject.h b/Include/internal/pycore_bytesobject.h index 27a7a46152f57b8..258a7fd19eefeca 100644 --- a/Include/internal/pycore_bytesobject.h +++ b/Include/internal/pycore_bytesobject.h @@ -18,6 +18,7 @@ extern PyObject* _PyBytes_FormatEx( * specializing interpreter. Unlike PyBytes_Concat(), this returns a new * reference rather than modifying its first argument in place. */ extern PyObject* _PyBytes_Concat(PyObject *a, PyObject *b); +PyAPI_FUNC(PyObject *) _PyBytes_BinarySlice(PyObject *, PyObject *, PyObject *); extern PyObject* _PyBytes_FromHex( PyObject *string, diff --git a/Lib/test/test_bytes.py b/Lib/test/test_bytes.py index 1b9918c6c8f473c..8167472ac100598 100644 --- a/Lib/test/test_bytes.py +++ b/Lib/test/test_bytes.py @@ -1168,6 +1168,58 @@ def test_getitem_error(self): with self.assertRaisesRegex(TypeError, msg): b['a'] + def test_binary_slice(self): + def binary_slice(data, start, stop): + return data[start:stop] + + data = b'0123456789' + indices = (None, 0, 1, 5, 10, 20, -1, -5, -10, -20, + sys.maxsize, -sys.maxsize - 1, 10**100, -10**100) + for start in indices: + for stop in indices: + with self.subTest(start=start, stop=stop): + self.assertEqual(binary_slice(data, start, stop), + data[slice(start, stop)]) + + self.assertIs(binary_slice(data, None, None), data) + + calls = [] + + class Index: + def __init__(self, name, value): + self.name = name + self.value = value + + def __index__(self): + calls.append(self.name) + return self.value + + self.assertEqual(binary_slice(data, Index('start', 2), + Index('stop', 5)), b'234') + self.assertEqual(calls, ['start', 'stop']) + + calls.clear() + + class BadIndex: + def __index__(self): + calls.append('start') + raise ValueError('bad index') + + with self.assertRaisesRegex(ValueError, 'bad index'): + binary_slice(data, BadIndex(), Index('stop', 5)) + self.assertEqual(calls, ['start']) + + msg = "slice indices must be integers or have an __index__ method" + with self.assertRaisesRegex(TypeError, msg): + binary_slice(data, 1.5, 5) + + class SliceOverride(bytes): + def __getitem__(self, key): + return key + + key = binary_slice(SliceOverride(data), 2, 5) + self.assertEqual(key, slice(2, 5)) + def test_buffer_is_readonly(self): fd = os.open(__file__, os.O_RDONLY) with open(fd, "rb", buffering=0) as f: diff --git a/Lib/test/test_capi/test_opt.py b/Lib/test/test_capi/test_opt.py index 36efab518781410..9dc79fe2d481223 100644 --- a/Lib/test/test_capi/test_opt.py +++ b/Lib/test/test_capi/test_opt.py @@ -4599,6 +4599,26 @@ def f(n): self.assertIn("_UNPACK_SEQUENCE_TWO_TUPLE", uops) self.assertNotIn("_GUARD_TOS_TUPLE", uops) + def test_binary_slice_type_propagation(self): + def f(n): + result = None + for i in range(n): + stop = (i == TIER2_THRESHOLD) + 2 + result = ( + type("abc"[:stop]), + type([1, 2, 3][:stop]), + type((1, 2, 3)[:stop]), + type(b"abc"[:stop]), + ) + return result + + res, ex = self._run_with_optimizer(f, TIER2_THRESHOLD) + self.assertEqual(res, (str, list, tuple, bytes)) + self.assertIsNotNone(ex) + uops = get_opnames(ex) + self.assertEqual(count_ops(ex, "_BINARY_SLICE"), 4) + self.assertNotIn("_CALL_TYPE_1", 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 diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-08-24-15-30-00.gh-issue-144569.Bn7qKx.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-24-15-30-00.gh-issue-144569.Bn7qKx.rst new file mode 100644 index 000000000000000..d6a7d174c2f955a --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-24-15-30-00.gh-issue-144569.Bn7qKx.rst @@ -0,0 +1,2 @@ +Optimize ``BINARY_SLICE`` for :class:`bytes` by avoiding temporary +:class:`slice` object creation. diff --git a/Modules/_testinternalcapi/test_cases.c.h b/Modules/_testinternalcapi/test_cases.c.h index 7a75e80298fcd82..869f56e6bf2847f 100644 --- a/Modules/_testinternalcapi/test_cases.c.h +++ b/Modules/_testinternalcapi/test_cases.c.h @@ -1448,6 +1448,12 @@ res_o = _PyUnicode_BinarySlice(container_o, start_o, stop_o); _PyFrame_StackPointerInvalidate(frame); } + else if (PyBytes_CheckExact(container_o)) { + _PyFrame_SetStackPointer(frame, stack_pointer); + _PyFrame_StackPointerValidate(frame); + res_o = _PyBytes_BinarySlice(container_o, start_o, stop_o); + _PyFrame_StackPointerInvalidate(frame); + } else { PyObject *slice = PySlice_New(start_o, stop_o, NULL); if (slice == NULL) { diff --git a/Objects/bytesobject.c b/Objects/bytesobject.c index ef35dad82e8aaea..cf3aee126596a75 100644 --- a/Objects/bytesobject.c +++ b/Objects/bytesobject.c @@ -1729,6 +1729,19 @@ bytes_hash(PyObject *self) return hash; } +static PyObject * +bytes_slice(PyObject *op, Py_ssize_t start, Py_ssize_t length) +{ + if (length <= 0) { + return Py_GetConstant(Py_CONSTANT_EMPTY_BYTES); + } + if (start == 0 && length == PyBytes_GET_SIZE(op) && + PyBytes_CheckExact(op)) { + return Py_NewRef(op); + } + return PyBytes_FromStringAndSize(PyBytes_AS_STRING(op) + start, length); +} + static PyObject* bytes_subscript(PyObject *op, PyObject* item) { @@ -1759,18 +1772,11 @@ bytes_subscript(PyObject *op, PyObject* item) slicelength = PySlice_AdjustIndices(PyBytes_GET_SIZE(self), &start, &stop, step); - if (slicelength <= 0) { - return Py_GetConstant(Py_CONSTANT_EMPTY_BYTES); - } - else if (start == 0 && step == 1 && - slicelength == PyBytes_GET_SIZE(self) && - PyBytes_CheckExact(self)) { - return Py_NewRef(self); + if (step == 1) { + return bytes_slice(op, start, slicelength); } - else if (step == 1) { - return PyBytes_FromStringAndSize( - PyBytes_AS_STRING(self) + start, - slicelength); + else if (slicelength <= 0) { + return Py_GetConstant(Py_CONSTANT_EMPTY_BYTES); } else { source_buf = PyBytes_AS_STRING(self); @@ -1795,6 +1801,18 @@ bytes_subscript(PyObject *op, PyObject* item) } } +PyObject * +_PyBytes_BinarySlice(PyObject *container, PyObject *start_o, PyObject *stop_o) +{ + assert(PyBytes_CheckExact(container)); + Py_ssize_t len = PyBytes_GET_SIZE(container); + Py_ssize_t istart, istop; + if (!_PyEval_UnpackIndices(start_o, stop_o, len, &istart, &istop)) { + return NULL; + } + return bytes_slice(container, istart, istop - istart); +} + static int bytes_buffer_getbuffer(PyObject *op, Py_buffer *view, int flags) { diff --git a/Python/bytecodes.c b/Python/bytecodes.c index fb0cdf4d65e060d..cfdba78014f0f47 100644 --- a/Python/bytecodes.c +++ b/Python/bytecodes.c @@ -1096,6 +1096,9 @@ dummy_func( else if (PyUnicode_CheckExact(container_o)) { res_o = _PyUnicode_BinarySlice(container_o, start_o, stop_o); } + else if (PyBytes_CheckExact(container_o)) { + res_o = _PyBytes_BinarySlice(container_o, start_o, stop_o); + } else { PyObject *slice = PySlice_New(start_o, stop_o, NULL); if (slice == NULL) { diff --git a/Python/executor_cases.c.h b/Python/executor_cases.c.h index 9aad9e003765cf8..2ca2531b4c312f7 100644 --- a/Python/executor_cases.c.h +++ b/Python/executor_cases.c.h @@ -6849,6 +6849,17 @@ res_o = _PyUnicode_BinarySlice(container_o, start_o, stop_o); _PyFrame_StackPointerInvalidate(frame); } + else if (PyBytes_CheckExact(container_o)) { + stack_pointer[0] = container; + stack_pointer[1] = start; + stack_pointer[2] = stop; + stack_pointer += 3; + ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); + _PyFrame_SetStackPointer(frame, stack_pointer); + _PyFrame_StackPointerValidate(frame); + res_o = _PyBytes_BinarySlice(container_o, start_o, stop_o); + _PyFrame_StackPointerInvalidate(frame); + } else { PyObject *slice = PySlice_New(start_o, stop_o, NULL); if (slice == NULL) { diff --git a/Python/generated_cases.c.h b/Python/generated_cases.c.h index 77c18b3d61fefc7..829fa27bf354312 100644 --- a/Python/generated_cases.c.h +++ b/Python/generated_cases.c.h @@ -1448,6 +1448,12 @@ res_o = _PyUnicode_BinarySlice(container_o, start_o, stop_o); _PyFrame_StackPointerInvalidate(frame); } + else if (PyBytes_CheckExact(container_o)) { + _PyFrame_SetStackPointer(frame, stack_pointer); + _PyFrame_StackPointerValidate(frame); + res_o = _PyBytes_BinarySlice(container_o, start_o, stop_o); + _PyFrame_StackPointerInvalidate(frame); + } else { PyObject *slice = PySlice_New(start_o, stop_o, NULL); if (slice == NULL) { diff --git a/Python/optimizer_bytecodes.c b/Python/optimizer_bytecodes.c index 5246e50633461bd..5b43d5a53760d22 100644 --- a/Python/optimizer_bytecodes.c +++ b/Python/optimizer_bytecodes.c @@ -2469,11 +2469,12 @@ dummy_func(void) { } op(_BINARY_SLICE, (container, start, stop -- res)) { - // Slicing a string/list/tuple always returns the same type. + // Slicing a string/list/tuple/bytes always returns the same type. PyTypeObject *type = sym_get_type(container); if (type == &PyUnicode_Type || type == &PyList_Type || - type == &PyTuple_Type) + type == &PyTuple_Type || + type == &PyBytes_Type) { res = sym_new_type(ctx, type); } diff --git a/Python/optimizer_cases.c.h b/Python/optimizer_cases.c.h index 21f275f27cafe04..dded0045e0bfc46 100644 --- a/Python/optimizer_cases.c.h +++ b/Python/optimizer_cases.c.h @@ -1306,7 +1306,8 @@ PyTypeObject *type = sym_get_type(container); if (type == &PyUnicode_Type || type == &PyList_Type || - type == &PyTuple_Type) + type == &PyTuple_Type || + type == &PyBytes_Type) { res = sym_new_type(ctx, type); }