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:
parent
d325ee4509
commit
519c24dd48
107
py/gc.c
107
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);
|
||||
}
|
||||
|
||||
|
|
8
py/gc.h
8
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.
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue