Skip to content

Commit f9a00c8

Browse files
committed
gh-144569: Avoid creating temporary objects in BINARY_SLICE for bytes
1 parent 53d2e14 commit f9a00c8

10 files changed

Lines changed: 115 additions & 14 deletions

File tree

Include/internal/pycore_bytesobject.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ extern PyObject* _PyBytes_FormatEx(
1818
* specializing interpreter. Unlike PyBytes_Concat(), this returns a new
1919
* reference rather than modifying its first argument in place. */
2020
extern PyObject* _PyBytes_Concat(PyObject *a, PyObject *b);
21+
PyAPI_FUNC(PyObject *) _PyBytes_BinarySlice(PyObject *, PyObject *, PyObject *);
2122

2223
extern PyObject* _PyBytes_FromHex(
2324
PyObject *string,

Lib/test/test_bytes.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1168,6 +1168,58 @@ def test_getitem_error(self):
11681168
with self.assertRaisesRegex(TypeError, msg):
11691169
b['a']
11701170

1171+
def test_binary_slice(self):
1172+
def binary_slice(data, start, stop):
1173+
return data[start:stop]
1174+
1175+
data = b'0123456789'
1176+
indices = (None, 0, 1, 5, 10, 20, -1, -5, -10, -20,
1177+
sys.maxsize, -sys.maxsize - 1, 10**100, -10**100)
1178+
for start in indices:
1179+
for stop in indices:
1180+
with self.subTest(start=start, stop=stop):
1181+
self.assertEqual(binary_slice(data, start, stop),
1182+
data[slice(start, stop)])
1183+
1184+
self.assertIs(binary_slice(data, None, None), data)
1185+
1186+
calls = []
1187+
1188+
class Index:
1189+
def __init__(self, name, value):
1190+
self.name = name
1191+
self.value = value
1192+
1193+
def __index__(self):
1194+
calls.append(self.name)
1195+
return self.value
1196+
1197+
self.assertEqual(binary_slice(data, Index('start', 2),
1198+
Index('stop', 5)), b'234')
1199+
self.assertEqual(calls, ['start', 'stop'])
1200+
1201+
calls.clear()
1202+
1203+
class BadIndex:
1204+
def __index__(self):
1205+
calls.append('start')
1206+
raise ValueError('bad index')
1207+
1208+
with self.assertRaisesRegex(ValueError, 'bad index'):
1209+
binary_slice(data, BadIndex(), Index('stop', 5))
1210+
self.assertEqual(calls, ['start'])
1211+
1212+
msg = "slice indices must be integers or have an __index__ method"
1213+
with self.assertRaisesRegex(TypeError, msg):
1214+
binary_slice(data, 1.5, 5)
1215+
1216+
class SliceOverride(bytes):
1217+
def __getitem__(self, key):
1218+
return key
1219+
1220+
key = binary_slice(SliceOverride(data), 2, 5)
1221+
self.assertEqual(key, slice(2, 5))
1222+
11711223
def test_buffer_is_readonly(self):
11721224
fd = os.open(__file__, os.O_RDONLY)
11731225
with open(fd, "rb", buffering=0) as f:
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Optimize ``BINARY_SLICE`` for :class:`bytes` by avoiding temporary
2+
:class:`slice` object creation.

Modules/_testinternalcapi/test_cases.c.h

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Objects/bytesobject.c

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1729,6 +1729,19 @@ bytes_hash(PyObject *self)
17291729
return hash;
17301730
}
17311731

1732+
static PyObject *
1733+
bytes_slice(PyObject *op, Py_ssize_t start, Py_ssize_t length)
1734+
{
1735+
if (length <= 0) {
1736+
return Py_GetConstant(Py_CONSTANT_EMPTY_BYTES);
1737+
}
1738+
if (start == 0 && length == PyBytes_GET_SIZE(op) &&
1739+
PyBytes_CheckExact(op)) {
1740+
return Py_NewRef(op);
1741+
}
1742+
return PyBytes_FromStringAndSize(PyBytes_AS_STRING(op) + start, length);
1743+
}
1744+
17321745
static PyObject*
17331746
bytes_subscript(PyObject *op, PyObject* item)
17341747
{
@@ -1759,18 +1772,11 @@ bytes_subscript(PyObject *op, PyObject* item)
17591772
slicelength = PySlice_AdjustIndices(PyBytes_GET_SIZE(self), &start,
17601773
&stop, step);
17611774

1762-
if (slicelength <= 0) {
1763-
return Py_GetConstant(Py_CONSTANT_EMPTY_BYTES);
1764-
}
1765-
else if (start == 0 && step == 1 &&
1766-
slicelength == PyBytes_GET_SIZE(self) &&
1767-
PyBytes_CheckExact(self)) {
1768-
return Py_NewRef(self);
1775+
if (step == 1) {
1776+
return bytes_slice(op, start, slicelength);
17691777
}
1770-
else if (step == 1) {
1771-
return PyBytes_FromStringAndSize(
1772-
PyBytes_AS_STRING(self) + start,
1773-
slicelength);
1778+
else if (slicelength <= 0) {
1779+
return Py_GetConstant(Py_CONSTANT_EMPTY_BYTES);
17741780
}
17751781
else {
17761782
source_buf = PyBytes_AS_STRING(self);
@@ -1795,6 +1801,18 @@ bytes_subscript(PyObject *op, PyObject* item)
17951801
}
17961802
}
17971803

1804+
PyObject *
1805+
_PyBytes_BinarySlice(PyObject *container, PyObject *start_o, PyObject *stop_o)
1806+
{
1807+
assert(PyBytes_CheckExact(container));
1808+
Py_ssize_t len = PyBytes_GET_SIZE(container);
1809+
Py_ssize_t istart, istop;
1810+
if (!_PyEval_UnpackIndices(start_o, stop_o, len, &istart, &istop)) {
1811+
return NULL;
1812+
}
1813+
return bytes_slice(container, istart, istop - istart);
1814+
}
1815+
17981816
static int
17991817
bytes_buffer_getbuffer(PyObject *op, Py_buffer *view, int flags)
18001818
{

Python/bytecodes.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1096,6 +1096,9 @@ dummy_func(
10961096
else if (PyUnicode_CheckExact(container_o)) {
10971097
res_o = _PyUnicode_BinarySlice(container_o, start_o, stop_o);
10981098
}
1099+
else if (PyBytes_CheckExact(container_o)) {
1100+
res_o = _PyBytes_BinarySlice(container_o, start_o, stop_o);
1101+
}
10991102
else {
11001103
PyObject *slice = PySlice_New(start_o, stop_o, NULL);
11011104
if (slice == NULL) {

Python/executor_cases.c.h

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Python/generated_cases.c.h

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Python/optimizer_bytecodes.c

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2469,11 +2469,12 @@ dummy_func(void) {
24692469
}
24702470

24712471
op(_BINARY_SLICE, (container, start, stop -- res)) {
2472-
// Slicing a string/list/tuple always returns the same type.
2472+
// Slicing a string/list/tuple/bytes always returns the same type.
24732473
PyTypeObject *type = sym_get_type(container);
24742474
if (type == &PyUnicode_Type ||
24752475
type == &PyList_Type ||
2476-
type == &PyTuple_Type)
2476+
type == &PyTuple_Type ||
2477+
type == &PyBytes_Type)
24772478
{
24782479
res = sym_new_type(ctx, type);
24792480
}

Python/optimizer_cases.c.h

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)