Skip to content

Commit 6c1776f

Browse files
committed
py/objslice: Avoid heap-allocating slices for built-ins.
Assume that a slice object (created by the `BUILD_SLICE` op-code or the native emitter via `mp_obj_new_slice`) will be used to slice a built-in type. This assumes that all built-in type->subscr functions will: 1. Not cause another slice to be allocated in the meantime. 2. Not save a reference to the slice. This prevents a heap allocation, which for some cases (e.g. memoryview assignment) prevents allocation altogether. Only when this assumption is incorrect (i.e. slicing a user-defined type via the `__{get,set,del}item__` methods), then heap-allocate the slice object. This work was funded through GitHub Sponsors. Signed-off-by: Jim Mussared <jim.mussared@gmail.com>
1 parent fb7d211 commit 6c1776f

File tree

5 files changed

+73
-3
lines changed

5 files changed

+73
-3
lines changed

py/mpstate.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,8 @@ typedef struct _mp_state_thread_t {
256256
// Locking of the GC is done per thread.
257257
uint16_t gc_lock_depth;
258258

259+
mp_obj_slice_t slice;
260+
259261
////////////////////////////////////////////////////////////
260262
// START ROOT POINTER SECTION
261263
// Everything that needs GC scanning must start here, and

py/objslice.c

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,18 @@ MP_DEFINE_CONST_OBJ_TYPE(
109109
);
110110

111111
mp_obj_t mp_obj_new_slice(mp_obj_t ostart, mp_obj_t ostop, mp_obj_t ostep) {
112-
mp_obj_slice_t *o = mp_obj_malloc(mp_obj_slice_t, &mp_type_slice);
112+
// Use the thread-local slice object, assuming that the likely case is
113+
// that this will be passed to a built-in type->subscr which will:
114+
// 1. Not cause another slice to be allocated in the meantime.
115+
// 2. Not save a reference to the slice.
116+
// The only exception to this is instance_subscr which will copy the slice
117+
// object into the heap before calling the user-defined __
118+
// {get,set,del}item__.
119+
// This prevents allocation of slice objects for all built-in types, as
120+
// well as making some specific cases (e.g. memoryview or bytearray slice
121+
// assignment) completely allocation-free.
122+
mp_obj_slice_t *o = &MP_STATE_THREAD(slice);
123+
o->base.type = &mp_type_slice;
113124
o->start = ostart;
114125
o->stop = ostop;
115126
o->step = ostep;

py/objtype.c

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -837,6 +837,20 @@ STATIC mp_obj_t instance_subscr(mp_obj_t self_in, mp_obj_t index, mp_obj_t value
837837
return mp_obj_subscr(self->subobj[0], index, value);
838838
} else if (member[0] != MP_OBJ_NULL) {
839839
size_t n_args = value == MP_OBJ_NULL || value == MP_OBJ_SENTINEL ? 1 : 2;
840+
841+
#if MICROPY_PY_BUILTINS_SLICE
842+
if (mp_obj_is_type(index, &mp_type_slice)) {
843+
// By default the slice object will be thread-local under the
844+
// assumption that built-in subscr methods will be safe. However
845+
// in this case we're calling a user-defined {get,set,del}item
846+
// handler and cannot make any assumptions, so copy it to the heap.
847+
// See mp_obj_new_slice for details.
848+
mp_obj_slice_t *o = m_new(mp_obj_slice_t, 1);
849+
memcpy(o, MP_OBJ_TO_PTR(index), sizeof(mp_obj_slice_t));
850+
member[2] = MP_OBJ_FROM_PTR(o);
851+
}
852+
#endif
853+
840854
mp_obj_t ret = mp_call_method_n_kw(n_args, 0, member);
841855
if (value == MP_OBJ_SENTINEL) {
842856
return ret;

tests/basics/builtin_slice.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,36 @@
11
# test builtin slice
22

3+
# ensures that slices passed to user types are heap-allocated and can be
4+
# safely stored as well as not overriden by subsequent slices.
5+
36
# print slice
47
class A:
58
def __getitem__(self, idx):
6-
print(idx)
9+
print("get", idx)
10+
print("abc"[1:])
11+
print("get", idx)
12+
return idx
13+
14+
def __setitem__(self, idx, value):
15+
print("set", idx)
16+
print("abc"[1:])
17+
print("set", idx)
18+
self.saved_idx = idx
19+
return idx
20+
21+
def __delitem__(self, idx):
22+
print("del", idx)
23+
print("abc"[1:])
24+
print("del", idx)
725
return idx
8-
s = A()[1:2:3]
26+
27+
a = A()
28+
s = a[1:2:3]
29+
a[4:5:6] = s
30+
del a[7:8:9]
31+
32+
print(a.saved_idx)
33+
934

1035
# check type
1136
print(type(s) is slice)

tests/micropython/heapalloc_slice.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# slice operations that don't require allocation
2+
try:
3+
from micropython import heap_lock, heap_unlock
4+
except (ImportError, AttributeError):
5+
heap_lock = heap_unlock = lambda: 0
6+
7+
b = bytearray(range(10))
8+
9+
m = memoryview(b)
10+
11+
heap_lock()
12+
13+
b[3:5] = b"aa"
14+
m[5:7] = b"bb"
15+
16+
heap_unlock()
17+
18+
print(b)

0 commit comments

Comments
 (0)