Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Include/internal/pycore_bytesobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
52 changes: 52 additions & 0 deletions Lib/test/test_bytes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
20 changes: 20 additions & 0 deletions Lib/test/test_capi/test_opt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Optimize ``BINARY_SLICE`` for :class:`bytes` by avoiding temporary
:class:`slice` object creation.
6 changes: 6 additions & 0 deletions Modules/_testinternalcapi/test_cases.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

40 changes: 29 additions & 11 deletions Objects/bytesobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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);
Expand All @@ -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)
{
Expand Down
3 changes: 3 additions & 0 deletions Python/bytecodes.c
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
11 changes: 11 additions & 0 deletions Python/executor_cases.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions Python/generated_cases.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions Python/optimizer_bytecodes.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Comment on lines 2474 to 2480

@cocolato cocolato Aug 26, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found that we didn't test the optimizer in the previous PR. Could you please add a test like this:

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)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cocolato
Thank you. I hadn't seen the SC announcement when I opened this PR, and only found out about the JIT pause afterwards.
https://discuss.python.org/t/an-announcement-from-the-steering-council-regarding-the-jit-project/107638

This part is Tier 2 optimizer work, so I think it should stay on hold for now. I'll keep the non-JIT bytes fast path separate.

@cocolato cocolato Aug 26, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've confirmed this with the SC that the JIT part is just an extra branch change, so we can handle it in this PR :)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for confirming. I've added the optimizer type-propagation test.

Expand Down
3 changes: 2 additions & 1 deletion Python/optimizer_cases.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading