From d59ec3251ea70af8c6399f08f8387de2b6a0a4c9 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 21 Oct 2026 12:06:08 -0500 Subject: [PATCH] Grow small map entry arrays through 1, 2, 3, 4, 6, 8, 12 and 16 slots --- packages/runtime/src/scr_map.c | 15 +++++++++++---- packages/runtime/src/scr_map.test.c | 16 ++++++++-------- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/packages/runtime/src/scr_map.c b/packages/runtime/src/scr_map.c index ecd1a558b..5d4935ed0 100644 --- a/packages/runtime/src/scr_map.c +++ b/packages/runtime/src/scr_map.c @@ -702,11 +702,18 @@ static void scr_map_compact(ScrMap *m, bool reindex) { * range. */ #define SCR_MAP_MAX_ENTRIES ((size_t)1 << 30) -/* The first entry gets an array of one, so a one-entry map keeps no unused - * slots; the second grows it to four, and capacity doubles after that. */ +/* The entry capacity after `cap`: 1, 2, 3, 4, 6, 8, 12, 16, then doubling. + * Most maps hold one to four entries, so a small map keeps at most a third + * of its slots unused, and appends stay amortized constant. */ +static size_t scr_map_next_ecap(size_t cap) { + if (cap < 4) return cap + 1; + if (cap < 16) return cap < 8 ? cap + 2 : cap + 4; + return cap * 2; +} + static void scr_map_grow_entries(ScrMap *m) { - size_t cap = m->ecap == 0 ? 1 : m->ecap < 4 ? 4 : m->ecap; - while (cap < m->nentries + 1) cap *= 2; + size_t cap = scr_map_next_ecap(m->ecap); + while (cap < m->nentries + 1) cap = scr_map_next_ecap(cap); if (cap > SCR_MAP_MAX_ENTRIES) scr_map_oom(); ScrMapEntry *entries = scr_mem_realloc(m->entries, cap * scr_map_entry_size(m)); if (!entries) scr_map_oom(); diff --git a/packages/runtime/src/scr_map.test.c b/packages/runtime/src/scr_map.test.c index d6f1163f4..9916c7406 100644 --- a/packages/runtime/src/scr_map.test.c +++ b/packages/runtime/src/scr_map.test.c @@ -242,17 +242,17 @@ static void modes(void) { scr_map_release(set); } -/* Small maps keep compact storage: the entries array starts with one slot - * and grows to four, and maps of up to 8 entries keep no bucket table. */ +/* Small maps keep compact storage: the entries array grows through 1, 2, + * 3, 4, 6 and 8 slots, and maps of up to 8 entries keep no bucket table. */ static void small_storage(void) { ScrMap *map = scr_map_new(SCR_MAP_KEY_F64, SCR_MAP_VAL_F64, NULL); assert(!map->entries && map->ecap == 0); - scr_map_set_f64_f64(map, 100000, 1); - assert(map->ecap == 1); - scr_map_set_f64_f64(map, 200000, 2); - assert(map->ecap == 4); - for (int i = 3; i <= 8; i++) scr_map_set_f64_f64(map, i * 100000.0, i); - assert(map->ecap == 8 && !map->nbuckets && !map->ndense); + static const uint32_t caps[] = {1, 2, 3, 4, 6, 6, 8, 8}; + for (int i = 1; i <= 8; i++) { + scr_map_set_f64_f64(map, i * 100000.0, i); + assert(map->ecap == caps[i - 1]); + } + assert(!map->nbuckets && !map->ndense); double out; for (int i = 1; i <= 8; i++) assert(scr_map_get_f64_f64(map, i * 100000.0, &out) && out == i); assert(!scr_map_has_f64(map, 50000) && !scr_map_has_f64(map, 0));