Skip to content

Commit 89b3730

Browse files
authored
[mypyc] specialise bytearray(bytes()[i:j]) (#21901)
Fixes mypyc/mypyc#1216 Local compiled benchmarks showed approximately 1.8 to 2.1x speedup for 1 KiB and 1 MiB slices. Authored-by: Codex
1 parent 543dbce commit 89b3730

8 files changed

Lines changed: 368 additions & 4 deletions

File tree

mypyc/irbuild/specialize.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
MemberExpr,
3131
NameExpr,
3232
RefExpr,
33+
SliceExpr,
3334
StrExpr,
3435
SuperExpr,
3536
TupleExpr,
@@ -41,6 +42,7 @@
4142
Call,
4243
Extend,
4344
Integer,
45+
LoadErrorValue,
4446
PrimitiveDescription,
4547
RaiseStandardError,
4648
Register,
@@ -64,7 +66,9 @@
6466
int32_rprimitive,
6567
int64_rprimitive,
6668
int_rprimitive,
69+
is_any_int,
6770
is_bool_rprimitive,
71+
is_bytes_rprimitive,
6872
is_dict_rprimitive,
6973
is_fixed_width_rtype,
7074
is_float_rprimitive,
@@ -108,7 +112,7 @@
108112
vec_to_list,
109113
vec_to_tuple,
110114
)
111-
from mypyc.primitives.bytearray_ops import isinstance_bytearray
115+
from mypyc.primitives.bytearray_ops import bytearray_from_bytes_slice_op, isinstance_bytearray
112116
from mypyc.primitives.bytes_ops import (
113117
bytes_adjust_index_op,
114118
bytes_get_item_unsafe_op,
@@ -348,6 +352,38 @@ def translate_vec_to_list(builder: IRBuilder, expr: CallExpr, callee: RefExpr) -
348352
return None
349353

350354

355+
@specialize_function("builtins.bytearray")
356+
def translate_bytearray_from_bytes_slice(
357+
builder: IRBuilder, expr: CallExpr, callee: RefExpr
358+
) -> Value | None:
359+
"""Construct a bytearray from a bytes slice without an intermediate copy."""
360+
if len(expr.args) != 1 or expr.arg_kinds != [ARG_POS]:
361+
return None
362+
arg = expr.args[0]
363+
if not isinstance(arg, IndexExpr) or not is_bytes_rprimitive(builder.node_type(arg.base)):
364+
return None
365+
index = arg.index
366+
if (
367+
not isinstance(index, SliceExpr)
368+
or index.stride is not None
369+
or (index.begin_index is not None and not is_any_int(builder.node_type(index.begin_index)))
370+
or (index.end_index is not None and not is_any_int(builder.node_type(index.end_index)))
371+
):
372+
return None
373+
374+
obj = builder.accept(arg.base)
375+
# Use the default-argument sentinel so subclass slicing still receives None.
376+
if index.begin_index is None:
377+
start = builder.add(LoadErrorValue(int_rprimitive, is_borrowed=True))
378+
else:
379+
start = builder.accept(index.begin_index)
380+
if index.end_index is None:
381+
end = builder.add(LoadErrorValue(int_rprimitive, is_borrowed=True))
382+
else:
383+
end = builder.accept(index.end_index)
384+
return builder.primitive_op(bytearray_from_bytes_slice_op, [obj, start, end], expr.line)
385+
386+
351387
@specialize_function("builtins.list")
352388
def dict_methods_fast_path(builder: IRBuilder, expr: CallExpr, callee: RefExpr) -> Value | None:
353389
"""Specialize a common case when list() is called on a dictionary

mypyc/lib-rt/CPy.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -658,6 +658,7 @@ PyObject *CPyObject_GetAttr3(PyObject *v, PyObject *name, PyObject *defl);
658658
PyObject *CPyIter_Next(PyObject *iter);
659659
PyObject *CPyNumber_Power(PyObject *base, PyObject *index);
660660
PyObject *CPyNumber_InPlacePower(PyObject *base, PyObject *index);
661+
// An omitted slice bound is represented by CPY_INT_TAG.
661662
PyObject *CPyObject_GetSlice(PyObject *obj, CPyTagged start, CPyTagged end);
662663

663664

mypyc/lib-rt/bytearray_extra_ops.c

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,31 @@
33
PyObject *CPyByteArray_New(void) {
44
return PyByteArray_FromStringAndSize(NULL, 0);
55
}
6+
7+
PyObject *CPyByteArray_FromBytesSlice(PyObject *obj, CPyTagged start, CPyTagged end) {
8+
if (PyBytes_CheckExact(obj)
9+
&& (start == CPY_INT_TAG || CPyTagged_CheckShort(start))
10+
&& (end == CPY_INT_TAG || CPyTagged_CheckShort(end))) {
11+
Py_ssize_t size = PyBytes_GET_SIZE(obj);
12+
Py_ssize_t startn = start == CPY_INT_TAG ? 0 : CPyTagged_ShortAsSsize_t(start);
13+
Py_ssize_t endn = end == CPY_INT_TAG ? size : CPyTagged_ShortAsSsize_t(end);
14+
if (startn < 0) {
15+
startn += size;
16+
}
17+
if (endn < 0) {
18+
endn += size;
19+
}
20+
if (0 <= startn && startn <= endn && endn <= size) {
21+
return PyByteArray_FromStringAndSize(PyBytes_AS_STRING(obj) + startn, endn - startn);
22+
}
23+
}
24+
25+
// Preserve general slice semantics, including bytes subclass overrides.
26+
PyObject *slice = CPyObject_GetSlice(obj, start, end);
27+
if (slice == NULL) {
28+
return NULL;
29+
}
30+
PyObject *result = PyByteArray_FromObject(slice);
31+
Py_DECREF(slice);
32+
return result;
33+
}

mypyc/lib-rt/bytearray_extra_ops.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,8 @@
77
// Construct empty bytearray
88
PyObject *CPyByteArray_New(void);
99

10+
// Construct a bytearray from a bytes slice, avoiding an intermediate bytes object.
11+
// An omitted bound is represented by CPY_INT_TAG.
12+
PyObject *CPyByteArray_FromBytesSlice(PyObject *obj, CPyTagged start, CPyTagged end);
13+
1014
#endif

mypyc/lib-rt/generic_ops.c

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,11 @@ PyObject *CPyNumber_InPlacePower(PyObject *base, PyObject *index)
4747
}
4848

4949
PyObject *CPyObject_GetSlice(PyObject *obj, CPyTagged start, CPyTagged end) {
50-
PyObject *start_obj = CPyTagged_AsObject(start);
51-
PyObject *end_obj = CPyTagged_AsObject(end);
50+
PyObject *start_obj = start == CPY_INT_TAG ? Py_NewRef(Py_None) : CPyTagged_AsObject(start);
51+
PyObject *end_obj = end == CPY_INT_TAG ? Py_NewRef(Py_None) : CPyTagged_AsObject(end);
5252
if (unlikely(start_obj == NULL || end_obj == NULL)) {
53+
Py_XDECREF(start_obj);
54+
Py_XDECREF(end_obj);
5355
return NULL;
5456
}
5557
PyObject *slice = PySlice_New(start_obj, end_obj, NULL);

mypyc/primitives/bytearray_ops.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,13 @@
99

1010
from mypyc.ir.deps import BYTEARRAY_EXTRA_OPS
1111
from mypyc.ir.ops import ERR_MAGIC, ERR_NEVER
12-
from mypyc.ir.rtypes import bit_rprimitive, bytearray_rprimitive, object_rprimitive
12+
from mypyc.ir.rtypes import (
13+
bit_rprimitive,
14+
bytearray_rprimitive,
15+
bytes_rprimitive,
16+
int_rprimitive,
17+
object_rprimitive,
18+
)
1319
from mypyc.primitives.registry import custom_primitive_op, function_op, load_address_op
1420

1521
# Get the 'bytearray' type object.
@@ -24,6 +30,17 @@
2430
error_kind=ERR_MAGIC,
2531
)
2632

33+
# bytearray(bytes[start:end])
34+
# Omitted bounds use the tagged integer error value.
35+
bytearray_from_bytes_slice_op = custom_primitive_op(
36+
name="bytearray_from_bytes_slice",
37+
arg_types=[bytes_rprimitive, int_rprimitive, int_rprimitive],
38+
return_type=bytearray_rprimitive,
39+
c_function_name="CPyByteArray_FromBytesSlice",
40+
error_kind=ERR_MAGIC,
41+
dependencies=[BYTEARRAY_EXTRA_OPS],
42+
)
43+
2744
# bytearray() -- construct empty bytearray
2845
function_op(
2946
name="builtins.bytearray",

mypyc/test-data/irbuild-bytes.test

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,106 @@ L0:
9999
r0 = CPyBytes_GetSlice(a, start, end)
100100
return r0
101101

102+
[case testBytearrayFromBytesSlice]
103+
def f(a: bytes, start: int, end: int) -> bytearray:
104+
return bytearray(a[start:end])
105+
106+
def from_start(a: bytes, start: int) -> bytearray:
107+
return bytearray(a[start:])
108+
109+
def to_end(a: bytes, end: int) -> bytearray:
110+
return bytearray(a[:end])
111+
112+
def full(a: bytes) -> bytearray:
113+
return bytearray(a[:])
114+
[out]
115+
def f(a, start, end):
116+
a :: bytes
117+
start, end :: int
118+
r0 :: bytearray
119+
L0:
120+
r0 = CPyByteArray_FromBytesSlice(a, start, end)
121+
return r0
122+
def from_start(a, start):
123+
a :: bytes
124+
start, r0 :: int
125+
r1 :: bytearray
126+
L0:
127+
r0 = <error> :: int
128+
r1 = CPyByteArray_FromBytesSlice(a, start, r0)
129+
return r1
130+
def to_end(a, end):
131+
a :: bytes
132+
end, r0 :: int
133+
r1 :: bytearray
134+
L0:
135+
r0 = <error> :: int
136+
r1 = CPyByteArray_FromBytesSlice(a, r0, end)
137+
return r1
138+
def full(a):
139+
a :: bytes
140+
r0, r1 :: int
141+
r2 :: bytearray
142+
L0:
143+
r0 = <error> :: int
144+
r1 = <error> :: int
145+
r2 = CPyByteArray_FromBytesSlice(a, r0, r1)
146+
return r2
147+
148+
[case testBytearrayFromBytesSliceFixedWidth_64bit]
149+
from mypy_extensions import i32, i64
150+
151+
def f(a: bytes, start: i32, end: i64) -> bytearray:
152+
return bytearray(a[start:end])
153+
[out]
154+
def f(a, start, end):
155+
a :: bytes
156+
start :: i32
157+
end :: i64
158+
r0 :: native_int
159+
r1 :: int
160+
r2, r3 :: bit
161+
r4, r5, r6 :: int
162+
r7 :: bytearray
163+
L0:
164+
r0 = extend signed start: i32 to native_int
165+
r1 = r0 << 1
166+
r2 = end <= 4611686018427387903 :: signed
167+
if r2 goto L1 else goto L2 :: bool
168+
L1:
169+
r3 = end >= -4611686018427387904 :: signed
170+
if r3 goto L3 else goto L2 :: bool
171+
L2:
172+
r4 = CPyTagged_FromInt64(end)
173+
r5 = r4
174+
goto L4
175+
L3:
176+
r6 = end << 1
177+
r5 = r6
178+
L4:
179+
r7 = CPyByteArray_FromBytesSlice(a, r1, r5)
180+
return r7
181+
182+
[case testBytearrayFromBytesSliceWithStep]
183+
def f(a: bytes, start: int, end: int, step: int) -> bytearray:
184+
return bytearray(a[start:end:step])
185+
[out]
186+
def f(a, start, end, step):
187+
a :: bytes
188+
start, end, step :: int
189+
r0, r1, r2, r3, r4 :: object
190+
r5 :: bytes
191+
r6 :: bytearray
192+
L0:
193+
r0 = box(int, start)
194+
r1 = box(int, end)
195+
r2 = box(int, step)
196+
r3 = PySlice_New(r0, r1, r2)
197+
r4 = PyObject_GetItem(a, r3)
198+
r5 = cast(bytes, r4)
199+
r6 = PyByteArray_FromObject(r5)
200+
return r6
201+
102202
[case testBytesIndex]
103203
from mypy_extensions import i64
104204

0 commit comments

Comments
 (0)