summaryrefslogtreecommitdiff
path: root/py/gc.c
diff options
context:
space:
mode:
authorAngus Gratton <angus@redyak.com.au>2023-08-02 16:49:44 +1000
committerDamien George <damien@micropython.org>2023-08-15 10:48:02 +1000
commit519c24dd48764f28593112dbeac7e30b8b2a6240 (patch)
tree9ca0435f8687854e29fc34cd92824089f2213c94 /py/gc.c
parentd325ee4509d8f68e341d5efc5ddbe3c30f86089e (diff)
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>
Diffstat (limited to 'py/gc.c')
-rw-r--r--py/gc.c107
1 files changed, 105 insertions, 2 deletions
diff --git a/py/gc.c b/py/gc.c
index 11be30dfe..b2e4aa4aa 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);
}