8000 py/gc & port/esp32: Add MICROPY_GC_SPLIT_HEAP_AUTO flag. · micropython/micropython@dd40e55 · GitHub
[go: up one dir, main page]

Skip to content

Commit dd40e55

Browse files
committed
py/gc & port/esp32: Add MICROPY_GC_SPLIT_HEAP_AUTO flag.
- When set, the heap is automatically extended with new areas on demand, and shrunk if a heap area becomes empty during a GC pass or soft reset. - Enable this new flag for esp32 port. Tested on ESP32 GENERIC_SPIRAM and GENERIC_S3 configurations. One possible minor bug, the size allocated for a new heap block to fulfill a large allocation is up to 64 bytes too large - haven't been able to 100% reconcile the heap sizing algorithm in gc_setup_area() with the new one in gc_try_add_heap() yet. However, figure it's better to err (slightly) too long than to risk having the allocation slightly too small under some circumstances. Currently there is no API to manually add a block of a given size to the heap, although that could easily be added if necessary. Signed-off-by: Angus Gratton <angus@redyak.com.au>
1 parent 8322afc commit dd40e55

File tree

7 files changed

+192
-17
lines changed

7 files changed

+192
-17
lines changed

docs/library/esp32.rst

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,18 @@ Functions
5151
buffers and other data. This data is useful to get a sense of how much memory
5252
is available to ESP-IDF and the networking stack in particular. It may shed
5353
some light on situations where ESP-IDF operations fail due to allocation failures.
54-
The information returned is *not* useful to troubleshoot Python allocation failures,
55-
use `micropython.mem_info()` instead.
5654

5755
The capabilities parameter corresponds to ESP-IDF's ``MALLOC_CAP_XXX`` values but the
5856
two most useful ones are predefined as `esp32.HEAP_DATA` for data heap regions and
5957
`esp32.HEAP_EXEC` for executable regions as used by the native code emitter.
6058

59+
Free IDF heap memory in the `esp32.HEAP_DATA` region is available to be
60+
automatically added to the MicroPython heap if the MicroPython heap is
61+
full. However, the information returned here is otherwise *not* useful to
62+
troubleshoot Python allocation failures, use `micropython.mem_info()`
63+
instead. The "max new split" value in `micropython.mem_info()` output
64+
corresponds to the largest free block in the ESP-IDF heap.
65+
6166
The return value is a list of 4-tuples, where each 4-tuple corresponds to one heap
6267
and contains: the total bytes, the free bytes, the largest free block, and
6368
the minimum free seen over time.

ports/esp32/gccollect.c

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,16 @@ void gc_collect(void) {
8080
}
8181

8282
#endif
83+
84+
#if MICROPY_GC_SPLIT_HEAP_AUTO
85+
86+
size_t gc_get_max_new_split(void)
87+
{
88+
// Need to allow some overhead for heap block metadata.
89+
// Am choosing a conservative value as TLSF impl is a bit opaque.
90+
static const size_t HEAP_METADATA_OVERHEAD = 64;
91+
size_t largest = heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT);
92+
return largest < HEAP_METADATA_OVERHEAD ? 0 : largest - HEAP_METADATA_OVERHEAD;
93+
}
94+
95+
#endif

ports/esp32/main.c

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -100,13 +100,9 @@ void mp_task(void *pvParameter) {
100100
ESP_LOGE("esp_init", "can't create event loop: 0x%x\n", err);
101101
}
102102

103-
// Allocate the uPy heap using malloc and get the largest available region,
104-
// limiting to 1/2 total available memory to leave memory for the OS.
105-
// When SPIRAM is enabled, this will allocate from SPIRAM.
106-
uint32_t caps = MALLOC_CAP_8BIT;
107-
size_t heap_total = heap_caps_get_total_size(caps);
108-
size_t mp_task_heap_size = MIN(heap_caps_get_largest_free_block(caps), heap_total / 2);
109-
void *mp_task_heap = heap_caps_malloc(mp_task_heap_size, caps);
103+
/* Heap size starts small, grows on demand */
104+
const size_t mp_task_heap_size = 64 * 1024;
105+
void *mp_task_heap = malloc(mp_task_heap_size);
110106

111107
soft_reset:
112108
// initialise the stack pointer for the main thread

ports/esp32/mpconfigport.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@
6868
#define MICROPY_PY_THREAD_GIL (1)
6969
#define MICROPY_PY_THREAD_GIL_VM_DIVISOR (32)
7070

71+
#define MICROPY_GC_SPLIT_HEAP (1)
72+
#define MICROPY_GC_SPLIT_HEAP_AUTO (1)
73+
7174
// extended modules
7275
#ifndef MICROPY_ESPNOW
7376
#define MICROPY_ESPNOW (1)

py/gc.c

Lines changed: 154 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@
7979
#define ATB_3_IS_FREE(a) (((a) & ATB_MASK_3) == 0)
8080

8181
#if MICROPY_GC_SPLIT_HEAP
82-
#define NEXT_AREA(area) (area->next)
82+
#define NEXT_AREA(area) ((area)->next)
8383
#else
8484
#define NEXT_AREA(area) (NULL)
8585
#endif
@@ -129,7 +129,13 @@ STATIC void gc_setup_area(mp_state_mem_area_t *area, void *start, void *end) {
129129
// => T = A * (1 + BLOCKS_PER_ATB / BLOCKS_PER_FTB + BLOCKS_PER_ATB * BYTES_PER_BLOCK)
130130
size_t total_byte_len = (byte *)end - (byte *)start;
131131
#if MICROPY_ENABLE_FINALISER
132-
area->gc_alloc_table_byte_len = (total_byte_len - ALLOC_TABLE_GAP_BYTE) * MP_BITS_PER_BYTE / (MP_BITS_PER_BYTE + MP_BITS_PER_BYTE * BLOCKS_PER_ATB / BLOCKS_PER_FTB + MP_BITS_PER_BYTE * BLOCKS_PER_ATB * BYTES_PER_BLOCK);
132+
area->gc_alloc_table_byte_len = (total_byte_len - ALLOC_TABLE_GAP_BYTE)
133+
* MP_BITS_PER_BYTE
134+
/ (
135+
MP_BITS_PER_BYTE
136+
+ MP_BITS_PER_BYTE * BLOCKS_PER_ATB / BLOCKS_PER_FTB
137+
+ MP_BITS_PER_BYTE * BLOCKS_PER_ATB * BYTES_PER_BLOCK
138+
);
133139
#else
134140
area->gc_alloc_table_byte_len = (total_byte_len - ALLOC_TABLE_GAP_BYTE) / (1 + MP_BITS_PER_BYTE / 2 * BYTES_PER_BLOCK);
135141
#endif
@@ -164,11 +170,19 @@ STATIC void gc_setup_area(mp_state_mem_area_t *area, void *start, void *end) {
164170
#endif
165171

166172
DEBUG_printf("GC layout:\n");
167-
DEBUG_printf(" alloc table at %p, length " UINT_FMT " bytes, " UINT_FMT " blocks\n", MP_STATE_MEM(area).gc_alloc_table_start, MP_STATE_MEM(area).gc_alloc_table_byte_len, MP_STATE_MEM(area).gc_alloc_table_byte_len * BLOCKS_PER_ATB);
173+
DEBUG_printf(" alloc table at %p, length " UINT_FMT " bytes, "
174+
UINT_FMT " blocks\n",
175+
area->gc_alloc_table_start, area->gc_alloc_table_byte_len,
176+
area->gc_alloc_table_byte_len * BLOCKS_PER_ATB);
168177
#if MICROPY_ENABLE_FINALISER
169-
DEBUG_printf(" finaliser table at %p, length " UINT_FMT " bytes, " UINT_FMT " blocks\n", MP_STATE_MEM(area).gc_finaliser_table_start, gc_finaliser_table_byte_len, gc_finaliser_table_byte_len * BLOCKS_PER_FTB);
178+
DEBUG_printf(" finaliser table at %p, length " UINT_FMT " bytes, "
179+
UINT_FMT " blocks\n", area->gc_finaliser_table_start,
180+
gc_finaliser_table_byte_len,
181+
gc_finaliser_table_byte_len * BLOCKS_PER_FTB);
170182
#endif
171-
DEBUG_printf(" pool at %p, length " UINT_FMT " bytes, " UINT_FMT " blocks\n", MP_STATE_MEM(area).gc_pool_start, gc_pool_block_len * BYTES_PER_BLOCK, gc_pool_block_len);
183+
DEBUG_printf(" pool at %p, length " UINT_FMT " bytes, "
184+
UINT_FMT " blocks\n", area->gc_pool_start,
185+
gc_pool_block_len * BYTES_PER_BLOCK, gc_pool_block_len);
172186
}
173187

174188
void gc_init(void *start, void *end) {
@@ -221,6 +235,107 @@ void gc_add(void *start, void *end) {
221235
// Add this area to the linked list
222236
prev_area->next = area;
223237
}
238+
239+
#if MICROPY_GC_SPLIT_HEAP_AUTO
240+
// Try to automatically add a heap area large enough to
241+
// fulfill 'failed_alloc'
242+
STATIC bool gc_try_add_heap(size_t failed_alloc)
243+
{
244+
// Equation to find a heap large enough to hold failed_alloc is derived from
245+
// the equation for gc_setup_area(), above:
246+
//
247+
// T = A + F + P
248+
// P == failed_alloc rounded up to next block size
249+
// A = P / (BLOCKS_PER_ATB * BYTES_PER_BLOCK
250+
// F = A * BLOCKS_PER_ATB / BLOCKS_PER_FTB
251+
size_t p_blocks = (failed_alloc + BYTES_PER_BLOCK - 1) / BYTES_PER_BLOCK;
252+
// Round up to use full allocation block(s)
253+
p_blocks = (p_blocks + BLOCKS_PER_ATB - 1) & ~(BLOCKS_PER_ATB - 1);
254+
255+
// Unclear why these extra blocks needed, but necessary for the new heap
256+
// pool to be large enough
257+
p_blocks += BLOCKS_PER_ATB;
258+
259+
size_t p = p_blocks * BYTES_PER_BLOCK; // should match gc_pool_end - gc_pool_start
260+
261+
size_t a = p_blocks / BLOCKS_PER_ATB;
262+
263+
#if MICROPY_ENABLE_FINALISER
264+
// should match gc_finaliser_table_byte_len
265+
size_t f = (p_blocks + BLOCKS_PER_FTB - 1) / BLOCKS_PER_FTB;
266+
#else
267+
size_t f = 0;
268+
#endif
269+
270+
// T = ... (plus implementation overheads)
271+
size_t needed = p + a + f + sizeof(mp_state_mem_area_t) + ALLOC_TABLE_GAP_BYTE;
272+
273+
size_t avail = gc_get_max_new_split();
274+
275+
DEBUG_printf("gc_try_add_heap failed_alloc " UINT_FMT ", "
276+
"p " UINT_FMT " bytes " UINT_FMT " blocks, "
277+
"a " UINT_FMT " bytes, "
278+
"f " UINT_FMT " bytes, "
279+
"needed " UINT_FMT ", avail " UINT_FMT " bytes \n",
280+
failed_alloc,
281+
p,
282+
p_blocks,
283+
a,
284+
f,
285+
needed,
286+
avail);
287+
288+
if (avail < needed) {
289+
return false; // Can't fit
290+
}
291+
292+
// Deciding how much to grow the total heap by each time is tricky:
293+
//
294+
// - Grow by too small amounts, leads to heap fragmentation issues.
295+
//
296+
// - Grow by too large amounts, may lead to system heap running out of
297+
// space.
298+
//
299+
// Currently, this implementation is:
300+
//
301+
// - At minimum, aim to double the total heap size each time we add a new
302+
// heap. i.e. without any large single allocations, total size will be
303+
// 64KB -> 128KB -> 256KB -> 512KB -> 1MB, etc
304+
//
305+
// - If the failed allocation is too large to fit in that size, the new
306+
// heap is made exactly large enough for that allocation. Future growth
307+
// will double the total heap size again.
308+
//
309+
// - If the new heap won't fit in the available free space, add the largest
310+
// new heap that will fit (this may lead to failed system heap allocations
311+
// elsewhere, but some allocation will likely fail in this circumstance!)
312+
size_t total_heap = 0;
313+
for (mp_state_mem_area_t *area = &MP_STATE_MEM(area);
314+
area != NULL;
315+
area = NEXT_AREA(area)) {
316+
total_heap += area->gc_pool_end - area->gc_alloc_table_start;
317+
total_heap += ALLOC_TABLE_GAP_BYTE + sizeof(mp_state_mem_area_t);
318+
}
319+
320+
DEBUG_printf("total_heap " UINT_FMT " bytes\n", total_heap);
321+
322+
size_t to_alloc = MIN(avail, MAX(total_heap, needed));
323+
324+
mp_state_mem_area_t *new_heap = malloc(to_alloc);
325+
326+
DEBUG_printf("gc_try_add_heap malloc " UINT_FMT " = %p\n",
327+
to_alloc, new_heap);
328+
329+
if (new_heap == NULL) {
330+
return false;
331+
}
332+
333+
gc_add(new_heap, (void *)new_heap + to_alloc);
334+
335+
return true;
336+
}
337+
#endif
338+
224339
#endif
225340

226341
void gc_lock(void) {
@@ -377,7 +492,13 @@ STATIC void gc_sweep(void) {
377492
#endif
378493
// free unmarked heads and their tails
379494
int free_tail = 0;
495+
#if MICROPY_GC_SPLIT_HEAP_AUTO
496+
mp_state_mem_area_t *prev_area = NULL;
497+
#endif
380498
for (mp_state_mem_area_t *area = &MP_STATE_MEM(area); area != NULL; area = NEXT_AREA(area)) {
499+
#if MICROPY_GC_SPLIT_HEAP_AUTO
500+
bool area_empty = true;
501+
#endif
381502
for (size_t block = 0; block < area->gc_alloc_table_byte_len * BLOCKS_PER_ATB; block++) {
382503
MICROPY_GC_HOOK_LOOP(block);
383504
switch (ATB_GET_KIND(area, block)) {
@@ -424,9 +545,23 @@ STATIC void gc_sweep(void) {
424545
case AT_MARK:
425546
ATB_MARK_TO_HEAD(area, block);
426547
free_tail = 0;
548+
#if MICROPY_GC_SPLIT_HEAP_AUTO
549+
area_empty = false;
550+
#endif
427551
break;
428552
}
429553
}
554+
555+
#if MICROPY_GC_SPLIT_HEAP_AUTO
556+
// Free any empty area, aside from the first one
557+
if (area_empty && prev_area != NULL) {
558+
DEBUG_printf("gc_sweep free empty area %p\n", area);
559+
NEXT_AREA(prev_area) = NEXT_AREA(area);
560+
free(area);
561+
area = prev_area;
562+
}
563+
prev_area = area;
564+
#endif
430565
}
431566
}
432567

@@ -609,6 +744,9 @@ void *gc_alloc(size_t n_bytes, unsigned int alloc_flags) {
609744
size_t start_block;
610745
size_t n_free;
611746
int collected = !MP_STATE_MEM(gc_auto_collect_enabled);
747+
#if MICROPY_GC_SPLIT_HEAP_AUTO
748+
bool added = false;
749+
#endif
612750

613751
#if MICROPY_GC_ALLOC_THRESHOLD
614752
if (!collected && MP_STATE_MEM(gc_alloc_amount) >= MP_STATE_MEM(gc_alloc_threshold)) {
@@ -654,6 +792,12 @@ void *gc_alloc(size_t n_bytes, unsigned int alloc_flags) {
654792
GC_EXIT();
655793
// nothing found!
656794
if (collected) {
795+
#if MICROPY_GC_SPLIT_HEAP_AUTO
796+
if (!added && gc_try_add_heap(n_bytes)) {
797+
added = true;
798+
continue;
799+
}
800+
#endif
657801
return NULL;
658802
}
659803
DEBUG_printf("gc_alloc(" UINT_FMT "): no free mem, triggering GC\n", n_bytes);
@@ -1024,9 +1168,12 @@ void *gc_realloc(void *ptr_in, size_t n_bytes, bool allow_move) {
10241168
void gc_dump_info(const mp_print_t *print) {
10251169
gc_info_t info;
10261170
gc_info(&info);
1027-
mp_printf(print, "GC: total: %u, used: %u, free: %u\n",
1171+
mp_printf(print, "GC: total: %u, used: %u, free: %u",
10281172
(uint)info.total, (uint)info.used, (uint)info.free);
1029-
mp_printf(print, " No. of 1-blocks: %u, 2-blocks: %u, max blk sz: %u, max free sz: %u\n",
1173+
#if MICROPY_GC_SPLIT_HEAP_AUTO
1174+
mp_printf(print, ", max new split: %u", (uint)gc_get_max_new_split());
1175+
#endif
1176+
mp_printf(print, "\n No. of 1-blocks: %u, 2-blocks: %u, max blk sz: %u, max free sz: %u\n",
10301177
(uint)info.num_1block, (uint)info.num_2block, (uint)info.max_block, (uint)info.max_free);
10311178
}
10321179

py/gc.h

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,13 @@ void gc_init(void *start, void *end);
3535
#if MICROPY_GC_SPLIT_HEAP
3636
// Used to add additional memory areas to the heap.
3737
void gc_add(void *start, void *end);
38-
#endif
38+
39+
#if MICROPY_GC_SPLIT_HEAP_AUTO
40+
// Port must implement this function to return the maximum available
41+
// block of RAM to allocate a new heap area into using malloc().
42+
size_t gc_get_max_new_split(void);
43+
#endif // MICROPY_GC_SPLIT_HEAP_AUTO
44+
#endif // MICROPY_GC_SPLIT_HEAP
3945

4046
// These lock/unlock functions can be nested.
4147
// They can be used to prevent the GC from allocating/freeing.

py/mpconfig.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -616,6 +616,11 @@
616616
#define MICROPY_GC_SPLIT_HEAP (0)
617617
#endif
618618

619+
// Whether regions should be added/removed from the split heap as needed.
620+
#ifndef MICROPY_GC_SPLIT_HEAP_AUTO
621+
#define MICROPY_GC_SPLIT_HEAP_AUTO (0)
622+
#endif
623+
619624
// Hook to run code during time consuming garbage collector operations
620625
// *i* is the loop index variable (e.g. can be used to run every x loops)
621626
#ifndef MICROPY_GC_HOOK_LOOP

0 commit comments

Comments
 (0)
0