py/gc: Add new MICROPY_GC_SPLIT_HEAP_AUTO "auto grow heap" mode.

When set, the split heap is automatically extended with new areas on
demand, and shrunk if a heap area becomes empty during a GC pass or soft
reset.

To save code size the size allocation for a new heap block (including
metadata) is estimated at 103% of the failed allocation, rather than
working from the more complex algorithm in gc_try_add_heap(). This appears
to work well except in the extreme limit case when almost all RAM is
exhausted (~last few hundred bytes). However in this case some allocation
is likely to fail soon anyhow.

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.

This work was funded through GitHub Sponsors.

Signed-off-by: Angus Gratton <angus@redyak.com.au>
This commit is contained in:
Angus Gratton 2023-08-02 16:49:44 +10:00 committed by Damien George
parent d325ee4509
commit 519c24dd48
3 changed files with 127 additions and 3 deletions

107
py/gc.c
View File

@ -236,6 +236,83 @@ void gc_add(void *start, void *end) {
// Add this area to the linked list
prev_area->next = area;
}
#if MICROPY_GC_SPLIT_HEAP_AUTO
// Try to automatically add a heap area large enough to fulfill 'failed_alloc'.
STATIC bool gc_try_add_heap(size_t failed_alloc) {
// 'needed' is the size of a heap large enough to hold failed_alloc, with
// the additional metadata overheads as calculated in gc_setup_area().
//
// Rather than reproduce all of that logic here, we approximate that adding
// (13/512) is enough overhead for sufficiently large heap areas (the
// overhead converges to 3/128, but there's some fixed overhead and some
// rounding up of partial block sizes).
size_t needed = failed_alloc + MAX(2048, failed_alloc * 13 / 512);
size_t avail = gc_get_max_new_split();
DEBUG_printf("gc_try_add_heap failed_alloc " UINT_FMT ", "
"needed " UINT_FMT ", avail " UINT_FMT " bytes \n",
failed_alloc,
needed,
avail);
if (avail < needed) {
// Can't fit this allocation, or system heap has nearly run out anyway
return false;
}
// Deciding how much to grow the total heap by each time is tricky:
//
// - Grow by too small amounts, leads to heap fragmentation issues.
//
// - Grow by too large amounts, may lead to system heap running out of
// space.
//
// Currently, this implementation is:
//
// - At minimum, aim to double the total heap size each time we add a new
// heap. i.e. without any large single allocations, total size will be
// 64KB -> 128KB -> 256KB -> 512KB -> 1MB, etc
//
// - If the failed allocation is too large to fit in that size, the new
// heap is made exactly large enough for that allocation. Future growth
// will double the total heap size again.
//
// - If the new heap won't fit in the available free space, add the largest
// new heap that will fit (this may lead to failed system heap allocations
// elsewhere, but some allocation will likely fail in this circumstance!)
size_t total_heap = 0;
for (mp_state_mem_area_t *area = &MP_STATE_MEM(area);
area != NULL;
area = NEXT_AREA(area)) {
total_heap += area->gc_pool_end - area->gc_alloc_table_start;
total_heap += ALLOC_TABLE_GAP_BYTE + sizeof(mp_state_mem_area_t);
}
DEBUG_printf("total_heap " UINT_FMT " bytes\n", total_heap);
size_t to_alloc = MIN(avail, MAX(total_heap, needed));
mp_state_mem_area_t *new_heap = MP_PLAT_ALLOC_HEAP(to_alloc);
DEBUG_printf("MP_PLAT_ALLOC_HEAP " UINT_FMT " = %p\n",
to_alloc, new_heap);
if (new_heap == NULL) {
// This should only fail:
// - In a threaded environment if another thread has
// allocated while this function ran.
// - If there is a bug in gc_get_max_new_split().
return false;
}
gc_add(new_heap, (void *)new_heap + to_alloc);
return true;
}
#endif
#endif
void gc_lock(void) {
@ -392,6 +469,9 @@ STATIC void gc_sweep(void) {
#endif
// free unmarked heads and their tails
int free_tail = 0;
#if MICROPY_GC_SPLIT_HEAP_AUTO
mp_state_mem_area_t *prev_area = NULL;
#endif
for (mp_state_mem_area_t *area = &MP_STATE_MEM(area); area != NULL; area = NEXT_AREA(area)) {
size_t end_block = area->gc_alloc_table_byte_len * BLOCKS_PER_ATB;
if (area->gc_last_used_block < end_block) {
@ -454,6 +534,17 @@ STATIC void gc_sweep(void) {
}
area->gc_last_used_block = last_used_block;
#if MICROPY_GC_SPLIT_HEAP_AUTO
// Free any empty area, aside from the first one
if (last_used_block == 0 && prev_area != NULL) {
DEBUG_printf("gc_sweep free empty area %p\n", area);
NEXT_AREA(prev_area) = NEXT_AREA(area);
MP_PLAT_FREE_HEAP(area);
area = prev_area;
}
prev_area = area;
#endif
}
}
@ -636,6 +727,9 @@ void *gc_alloc(size_t n_bytes, unsigned int alloc_flags) {
size_t start_block;
size_t n_free;
int collected = !MP_STATE_MEM(gc_auto_collect_enabled);
#if MICROPY_GC_SPLIT_HEAP_AUTO
bool added = false;
#endif
#if MICROPY_GC_ALLOC_THRESHOLD
if (!collected && MP_STATE_MEM(gc_alloc_amount) >= MP_STATE_MEM(gc_alloc_threshold)) {
@ -681,6 +775,12 @@ void *gc_alloc(size_t n_bytes, unsigned int alloc_flags) {
GC_EXIT();
// nothing found!
if (collected) {
#if MICROPY_GC_SPLIT_HEAP_AUTO
if (!added && gc_try_add_heap(n_bytes)) {
added = true;
continue;
}
#endif
return NULL;
}
DEBUG_printf("gc_alloc(" UINT_FMT "): no free mem, triggering GC\n", n_bytes);
@ -1056,9 +1156,12 @@ void *gc_realloc(void *ptr_in, size_t n_bytes, bool allow_move) {
void gc_dump_info(const mp_print_t *print) {
gc_info_t info;
gc_info(&info);
mp_printf(print, "GC: total: %u, used: %u, free: %u\n",
mp_printf(print, "GC: total: %u, used: %u, free: %u",
(uint)info.total, (uint)info.used, (uint)info.free);
mp_printf(print, " No. of 1-blocks: %u, 2-blocks: %u, max blk sz: %u, max free sz: %u\n",
#if MICROPY_GC_SPLIT_HEAP_AUTO
mp_printf(print, ", max new split: %u", (uint)gc_get_max_new_split());
#endif
mp_printf(print, "\n No. of 1-blocks: %u, 2-blocks: %u, max blk sz: %u, max free sz: %u\n",
(uint)info.num_1block, (uint)info.num_2block, (uint)info.max_block, (uint)info.max_free);
}

View File

@ -35,7 +35,13 @@ void gc_init(void *start, void *end);
#if MICROPY_GC_SPLIT_HEAP
// Used to add additional memory areas to the heap.
void gc_add(void *start, void *end);
#endif
#if MICROPY_GC_SPLIT_HEAP_AUTO
// Port must implement this function to return the maximum available block of
// RAM to allocate a new heap area into using MP_PLAT_ALLOC_HEAP.
size_t gc_get_max_new_split(void);
#endif // MICROPY_GC_SPLIT_HEAP_AUTO
#endif // MICROPY_GC_SPLIT_HEAP
// These lock/unlock functions can be nested.
// They can be used to prevent the GC from allocating/freeing.

View File

@ -616,6 +616,11 @@
#define MICROPY_GC_SPLIT_HEAP (0)
#endif
// Whether regions should be added/removed from the split heap as needed.
#ifndef MICROPY_GC_SPLIT_HEAP_AUTO
#define MICROPY_GC_SPLIT_HEAP_AUTO (0)
#endif
// Hook to run code during time consuming garbage collector operations
// *i* is the loop index variable (e.g. can be used to run every x loops)
#ifndef MICROPY_GC_HOOK_LOOP
@ -1896,6 +1901,16 @@ typedef double mp_float_t;
#define MP_PLAT_FREE_EXEC(ptr, size) m_del(byte, ptr, size)
#endif
// Allocating new heap area at runtime requires port to be able to allocate from system heap
#if MICROPY_GC_SPLIT_HEAP_AUTO
#ifndef MP_PLAT_ALLOC_HEAP
#define MP_PLAT_ALLOC_HEAP(size) malloc(size)
#endif
#ifndef MP_PLAT_FREE_HEAP
#define MP_PLAT_FREE_HEAP(ptr) free(ptr)
#endif
#endif
// This macro is used to do all output (except when MICROPY_PY_IO is defined)
#ifndef MP_PLAT_PRINT_STRN
#define MP_PLAT_PRINT_STRN(str, len) mp_hal_stdout_tx_strn_cooked(str, len)