diff --git a/py/gc.c b/py/gc.c index 11be30dfef..b2e4aa4aae 100644 --- a/py/gc.c +++ b/py/gc.c @@ -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); } diff --git a/py/gc.h b/py/gc.h index 8431c0a6cf..7eec6265c8 100644 --- a/py/gc.h +++ b/py/gc.h @@ -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. diff --git a/py/mpconfig.h b/py/mpconfig.h index 4a15205ae3..de4a89206b 100644 --- a/py/mpconfig.h +++ b/py/mpconfig.h @@ -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)