8000 py/gc: When heap autosplit is enabled, soft limit the new heap size. · micropython/micropython@de5fae0 · GitHub
[go: up one dir, main page]

Skip to content

Commit de5fae0

Browse files
committed
py/gc: When heap autosplit is enabled, soft limit the new heap size.
Previously the "auto split" function would try to "greedily" double the heap size each time, and only tries to grow by a smaller increment if it would be not possible to allocate the memory. Now it will stop adding large chunks of heap once the free system heap is lower than a limit. The limit is exposed as micropython.heap_sys_reserve() so it can be tweaked for particular firmware use cases. With this change and no tuning of the limit, the Python heap growth on original ESP32 is closer to the v1.20 Python heap size after the first time it expands. Signed-off-by: Angus Gratton <angus@redyak.com.au>
1 parent f3889db commit de5fae0

File tree

7 files changed

+115
-28
lines changed

7 files changed

+115
-28
lines changed

docs/library/esp32.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ Functions
7676
The "max new split" value in :func:`micropython.mem_info()` output
7777
corresponds to the largest free block of ESP-IDF heap that could be
7878
automatically added on demand to the MicroPython heap.
79+
See also :func:`micropython.heap_sys_reserve`.
7980

8081
The result of :func:`gc.mem_free()` is the total of the current "free"
8182
and "max new split" values printed by :func:`micropython.mem_info()`.

docs/library/micropython.rst

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,28 @@ Functions
102102
Note: `heap_locked()` is not enabled on most ports by default,
103103
requires ``MICROPY_PY_MICROPYTHON_HEAP_LOCKED``.
104104

105+
.. function:: heap_sys_reserve([new_value])
106+
107+
.. note:: This function is only present on ports using the "auto split heap"
108+
feature.
109+
110+
Get or set the number of bytes of free heap RAM that MicroPython *tries* to
111+
reserve for the system.
112+
113+
When called with no arguments, returns the currently set value. When called
114+
with an argument, updates the currently set value.
115+
116+
This is a soft limit that prevents "greedily" growing the MicroPython heap
117+
too large. If MicroPython has no choice but to grow the heap or fail then
118+
it will still grow the MicroPython heap beyond this limit.
119+
120+
Setting a higher value may help if the system is failing to allocate memory
121+
outside MicroPython. Setting a lower value or even zero may help if
122+
MicroPython memory is becoming unnecessarily fragmented.
123+
124+
Changing this limit cannot resolve memory issues that are caused by requiring
125+
more memory than is physically available in the system.
126+
105127
.. function:: kbd_intr(chr)
106128

107129
Set the character that will raise a `KeyboardInterrupt` exception. By

ports/esp32/gccollect.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,4 +84 8000 ,8 @@ size_t gc_get_max_new_split(void) {
8484
return heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT);
8585
}
8686

87+
size_t gc_get_total_free(void) {
88+
return heap_caps_get_free_size(MALLOC_CAP_DEFAULT);
89+
}
90+
8791
#endif

ports/esp32/mpconfigport.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,15 @@ void *esp_native_code_commit(void *, size_t, void *);
187187
#define MP_PLAT_COMMIT_EXEC(buf, len, reloc) esp_native_code_commit(buf, len, reloc)
188188
#define MP_SSIZE_MAX (0x7fffffff)
189189

190+
#ifndef MP_PLAT_DEFAULT_HEAP_SYS_RESERVE
191+
// Try to keep some heap free for ESP-IDF system, unless Python is completely out of memory
192+
//
193+
// The default here is particularly high, because it's enough memory to initialise Wi-Fi and
194+
// create a TLS connection. If the program is not using these things, or if the heap doesn't
195+
// grow until after those things are created, then this will fragment memory more than needed.
196+
#define MP_PLAT_DEFAULT_HEAP_SYS_RESERVE (80 * 1024)
197+
#endif
198+
190199
#if MICROPY_PY_SOCKET_EVENTS
191200
#define MICROPY_PY_SOCKET_EVENTS_HANDLER extern void socket_events_handler(void); socket_events_handler();
192201
#else

py/gc.c

Lines changed: 58 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,9 @@ void gc_add(void *start, void *end) {
238238
}
239239

240240
#if MICROPY_GC_SPLIT_HEAP_AUTO
241+
242+
size_t gc_heap_sys_reserve = MP_PLAT_DEFAULT_HEAP_SYS_RESERVE;
243+
241244
// Try to automatically add a heap area large enough to fulfill 'failed_alloc'.
242245
STATIC bool gc_try_add_heap(size_t failed_alloc) {
243246
// 'needed' is the size of a heap large enough to hold failed_alloc, with
@@ -249,55 +252,82 @@ STATIC bool gc_try_add_heap(size_t failed_alloc) {
249252
// rounding up of partial block sizes).
250253
size_t needed = failed_alloc + MAX(2048, failed_alloc * 13 / 512);
251254

252-
size_t avail = gc_get_max_new_split();
255+
size_t max_new_split = gc_get_max_new_split();
253256

254257
DEBUG_printf("gc_try_add_heap failed_alloc " UINT_FMT ", "
255-
"needed " UINT_FMT ", avail " UINT_FMT " bytes \n",
258+
"needed " UINT_FMT ", max_new_split " UINT_FMT " bytes \n",
256259
failed_alloc,
257260
needed,
258-
avail);
261+
max_new_split);
259262

260-
if (avail < needed) {
263+
if (max_new_split < needed) {
261264
// Can't fit this allocation, or system heap has nearly run out anyway
262265
return false;
263266
}
264267

265-
// Deciding how much to grow the total heap by each time is tricky:
268+
// Measure the total Python heap size
269+
size_t total_heap = 0;
270+
for (mp_state_mem_area_t *area = &MP_STATE_MEM(area);
271+
area != NULL;
272+
area = NEXT_AREA(area)) {
273+
total_heap += area->gc_pool_end - area->gc_alloc_table_start;
274+
total_heap += ALLOC_TABLE_GAP_BYTE + sizeof(mp_state_mem_area_t);
275+
}
276+
277+
// Deciding how much to allocate for the new "split" heap is tricky:
266278
//
267279
// - Grow by too small amounts, leads to heap fragmentation issues.
268280
//
269281
// - Grow by too large amounts, may lead to system heap running out of
270282
// space.
271283
//
272-
// Currently, this implementation is:
273-
//
274-
// - At minimum, aim to double the total heap size each time we add a new
275-
// heap. i.e. without any large single allocations, total size will be
276-
// 64KB -> 128KB -> 256KB -> 512KB -> 1MB, etc
277-
//
278-
// - If the failed allocation is too large to fit in that size, the new
279-
// heap is made exactly large enough for that allocation. Future growth
280-
// will double the total heap size again.
284+
285+
// Start by choosing the current total Python heap size for the new heap.
286+
// With no other constraints, the total Python heap size would double each
287+
// time: i.e 64KB -> 128KB -> 256KB -> 512KB -> 1MB, etc. This avoids
288+
// fragmentation where possible.
289+
290+
size_t new_heap_size = total_heap;
291+
292+
// If this "greedy" size will cut free system heap below
293+
// gc_heap_sys_reserve then reduce it to conserve that limit.
281294
//
282-
// - If the new heap won't fit in the available free space, add the largest
283-
// new heap that will fit (this may lead to failed system heap allocations
284-
// elsewhere, but some allocation will likely fail in this circumstance!)
285-
size_t total_heap = 0;
286-
for (mp_state_mem_area_t *area = &MP_STATE_MEM(area);
287-
area != NULL;
288-
area = NEXT_AREA(area)) {
289-
total_heap += area->gc_pool_end - area->gc_alloc_table_start;
290-
total_heap += ALLOC_TABLE_GAP_BYTE + sizeof(mp_state_mem_area_t);
295+
// (gc_heap_sys_reserve still isn't a hard limit, if the only
296+
// options are returning a MemoryError to Python or using up the reserved
297+
// system heap space then MicroPython will use up the reserved system heap
298+
// space.)
299+
300+
size_t total_free = gc_get_total_free();
301+
if (total_free < gc_heap_sys_reserve) {
302+
new_heap_size = needed;
303+
} else if (total_free - new_heap_size < gc_heap_sys_reserve) {
304+
new_heap_size = total_free - gc_heap_sys_reserve;
305+
}
306+
307+
// If this size is smaller than the size 'needed' to avoid an immediate
308+
// MemoryError, increase to this size so the current failing allocation
309+
// can succeed.
310+
311+
if (new_heap_size < needed) {
312+
new_heap_size = needed;
291313
}
292314

293-
DEBUG_printf("total_heap " UINT_FMT " bytes\n", total_heap);
315+
// If this size won't fit in the largest free system heap block, decrease
316+
// it so it will fit (note: due to the check earlier, we already know
317+
// max_new_split is large enough to hold 'needed')
318+
319+
if (new_heap_size > max_new_split) {
320+
new_heap_size = max_new_split;
321+
}
294322

295-
size_t to_alloc = MIN(avail, MAX(total_heap, needed));
323+
DEBUG_printf("total_heap " UINT_FMT " total_free "
324+
UINT_FMT " TRY_RESERVE_SYSTEM_HEAP " UINT_FMT " bytes\n",
325+
total_heap, total_free, gc_heap_sys_reserve);
296326

297-
mp_state_mem_area_t *new_heap = MP_PLAT_ALLOC_HEAP(to_alloc);
327+
mp_state_mem_area_t *new_heap = MP_PLAT_ALLOC_HEAP(new_heap_size);
298328

299329
DEBUG_printf("MP_PLAT_ALLOC_HEAP " UINT_FMT " = %p\n",
300-
to_alloc, new_heap);
330+
new_heap_size, new_heap);
301331

302332
if (new_heap == NULL) {
303333
// This should only fail:
@@ -307,7 +337,7 @@ STATIC bool gc_try_add_heap(size_t failed_alloc) {
307337
return false;
308338
}
309339

310-
gc_add(new_heap, (void *)new_heap + to_alloc);
340+
gc_add(new_heap, (void *)new_heap + ne FEE1 w_heap_size);
311341

312342
return true;
313343
}

py/gc.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ void gc_add(void *start, void *end);
4040
// Port must implement this function to return the maximum available block of
4141
// RAM to allocate a new heap area into using MP_PLAT_ALLOC_HEAP.
4242
size_t gc_get_max_new_split(void);
43+
// This function returns the total amount of free RAM available for heap.
44+
size_t gc_get_total_free(void);
45+
// Runtime tuneable "soft" limit for free system heap
46+
extern size_t gc_heap_sys_reserve;
4347
#endif // MICROPY_GC_SPLIT_HEAP_AUTO
4448
#endif // MICROPY_GC_SPLIT_HEAP
4549

py/modmicropython.c

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,20 @@ STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_micropython_heap_locked_obj, mp_micropython_
144144
#endif
145145
#endif
146146

147+
#if MICROPY_GC_SPLIT_HEAP_AUTO
148+
STATIC mp_obj_t mp_micropython_heap_sys_reserve(size_t n_args, const mp_obj_t *args) {
149+
if (n_args > 0) {
150+
mp_int_t new = mp_obj_get_int(args[0]);
151+
if (new < 0) {
152+
mp_raise_ValueError(NULL);
153+
}
154+
gc_heap_sys_reserve = new;
155+
}
156+
return mp_obj_new_int(gc_heap_sys_reserve);
157+
}
158+
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mp_micropython_heap_sys_reserve_obj, 0, 1, mp_micropython_heap_sys_reserve);
159+
#endif
160+
147161
#if MICROPY_ENABLE_EMERGENCY_EXCEPTION_BUF && (MICROPY_EMERGENCY_EXCEPTION_BUF_SIZE == 0)
148162
STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_alloc_emergency_exception_buf_obj, mp_alloc_emergency_exception_buf);
149163
#endif
@@ -196,6 +210,9 @@ STATIC const mp_rom_map_elem_t mp_module_micropython_globals_table[] = {
196210
#if MICROPY_PY_MICROPYTHON_HEAP_LOCKED
197211
{ MP_ROM_QSTR(MP_QSTR_heap_locked), MP_ROM_PTR(&mp_micropython_heap_locked_obj) },
198212
#endif
213+
#if MICROPY_GC_SPLIT_HEAP_AUTO
214+
{ MP_ROM_QSTR(MP_QSTR_heap_sys_reserve), MP_ROM_PTR(&mp_micropython_heap_sys_reserve_obj) },
215+
#endif
199216
#endif
200217
#if MICROPY_KBD_EXCEPTION
201218
{ MP_ROM_QSTR(MP_QSTR_kbd_intr), MP_ROM_PTR(&mp_micropython_kbd_intr_obj) },

0 commit comments

Comments
 (0)
0