diff options
| author | Jim Mussared <jim.mussared@gmail.com> | 2022-12-05 17:01:30 +1100 |
|---|---|---|
| committer | Damien George <damien@micropython.org> | 2025-07-16 00:12:47 +1000 |
| commit | 5e9189d6d1c00c92694888bf9c74276779c40716 (patch) | |
| tree | 46ac68ccf7dcf59bf53b2efea4c76fb267db133f | |
| parent | aa2362d4de3bed960b65c5e6e66544f68f2e7ed1 (diff) | |
py/vm: Avoid heap-allocating slices when subscripting built-ins.
This commit adds a fast-path optimisation for when a BUILD_SLICE is
immediately followed by a LOAD/STORE_SUBSCR for a native type, to avoid
needing to allocate the slice on the heap.
In some cases (e.g. `a[1:3] = x`) this can result in no allocations at all.
We can't do this for instance types because the get/set/delattr
implementation may keep a reference to the slice.
Adds more tests to the basic slice tests to ensure that a stack-allocated
slice never makes it to Python, and also a heapalloc test that verifies
(when using bytecode) that assigning to a slice is no-alloc.
This work was funded through GitHub Sponsors.
Signed-off-by: Jim Mussared <jim.mussared@gmail.com>
Signed-off-by: Damien George <damien@micropython.org>
| -rw-r--r-- | py/vm.c | 32 | ||||
| -rw-r--r-- | tests/basics/builtin_slice.py | 37 | ||||
| -rw-r--r-- | tests/micropython/heapalloc_slice.py | 18 | ||||
| -rwxr-xr-x | tests/run-tests.py | 3 |
4 files changed, 85 insertions, 5 deletions
@@ -195,6 +195,22 @@ #define TRACE_TICK(current_ip, current_sp, is_exception) #endif // MICROPY_PY_SYS_SETTRACE +#if MICROPY_PY_BUILTINS_SLICE +// This function is marked "no inline" so it doesn't increase the C stack usage of the main VM function. +MP_NOINLINE static mp_obj_t *build_slice_stack_allocated(byte op, mp_obj_t *sp, mp_obj_t step) { + mp_obj_t stop = sp[2]; + mp_obj_t start = sp[1]; + mp_obj_slice_t slice = { .base = { .type = &mp_type_slice }, .start = start, .stop = stop, .step = step }; + if (op == MP_BC_LOAD_SUBSCR) { + SET_TOP(mp_obj_subscr(TOP(), MP_OBJ_FROM_PTR(&slice), MP_OBJ_SENTINEL)); + } else { // MP_BC_STORE_SUBSCR + mp_obj_subscr(TOP(), MP_OBJ_FROM_PTR(&slice), sp[-1]); + sp -= 2; + } + return sp; +} +#endif + // fastn has items in reverse order (fastn[0] is local[0], fastn[-1] is local[1], etc) // sp points to bottom of stack which grows up // returns: @@ -849,9 +865,19 @@ unwind_jump:; // 3-argument slice includes step step = POP(); } - mp_obj_t stop = POP(); - mp_obj_t start = TOP(); - SET_TOP(mp_obj_new_slice(start, stop, step)); + if ((*ip == MP_BC_LOAD_SUBSCR || *ip == MP_BC_STORE_SUBSCR) && mp_obj_is_native_type(mp_obj_get_type(sp[-2]))) { + // Fast path optimisation for when the BUILD_SLICE is immediately followed + // by a LOAD/STORE_SUBSCR for a native type to avoid needing to allocate + // the slice on the heap. In some cases (e.g. a[1:3] = x) this can result + // in no allocations at all. We can't do this for instance types because + // the get/set/delattr implementation may keep a reference to the slice. + byte op = *ip++; + sp = build_slice_stack_allocated(op, sp - 2, step); + } else { + mp_obj_t stop = POP(); + mp_obj_t start = TOP(); + SET_TOP(mp_obj_new_slice(start, stop, step)); + } DISPATCH(); } #endif diff --git a/tests/basics/builtin_slice.py b/tests/basics/builtin_slice.py index df84d5c57..5197a7cad 100644 --- a/tests/basics/builtin_slice.py +++ b/tests/basics/builtin_slice.py @@ -1,11 +1,44 @@ # test builtin slice +# ensures that slices passed to user types are heap-allocated and can be +# safely stored as well as not overriden by subsequent slices. + # print slice class A: def __getitem__(self, idx): - print(idx) + print("get", idx) + print("abc"[1:]) + print("get", idx) + return idx + + def __setitem__(self, idx, value): + print("set", idx) + print("abc"[1:]) + print("set", idx) + self.saved_idx = idx + return idx + + def __delitem__(self, idx): + print("del", idx) + print("abc"[1:]) + print("del", idx) return idx -s = A()[1:2:3] + + +a = A() +s = a[1:2:3] +a[4:5:6] = s +del a[7:8:9] + +print(a.saved_idx) + +# nested slicing +print(A()[1 : A()[A()[2:3:4] : 5]]) + +# tuple slicing +a[1:2, 4:5, 7:8] +a[1, 4:5, 7:8, 2] +a[1:2, a[3:4], 5:6] # check type print(type(s) is slice) diff --git a/tests/micropython/heapalloc_slice.py b/tests/micropython/heapalloc_slice.py new file mode 100644 index 000000000..62d96595c --- /dev/null +++ b/tests/micropython/heapalloc_slice.py @@ -0,0 +1,18 @@ +# slice operations that don't require allocation +try: + from micropython import heap_lock, heap_unlock +except (ImportError, AttributeError): + heap_lock = heap_unlock = lambda: 0 + +b = bytearray(range(10)) + +m = memoryview(b) + +heap_lock() + +b[3:5] = b"aa" +m[5:7] = b"bb" + +heap_unlock() + +print(b) diff --git a/tests/run-tests.py b/tests/run-tests.py index 0eaee5278..fe338d7ff 100755 --- a/tests/run-tests.py +++ b/tests/run-tests.py @@ -855,6 +855,9 @@ def run_tests(pyb, tests, args, result_dir, num_threads=1): "micropython/emg_exc.py" ) # because native doesn't have proper traceback info skip_tests.add( + "micropython/heapalloc_slice.py" + ) # because native doesn't do the stack-allocated slice optimisation + skip_tests.add( "micropython/heapalloc_traceback.py" ) # because native doesn't have proper traceback info skip_tests.add( |
