pool.c (8322B)
1 /* Interned strings. Open-addressed hash table keyed by FNV-1a 2 * over the string body; the table holds Sym ids that index into a 3 * heap-allocated `entries` array. Strings live in a per-Pool arena 4 * (block-allocated), so str_data pointers are stable for the Pool's 5 * lifetime. */ 6 7 #include "core/pool.h" 8 9 #include <string.h> 10 11 #include "core/arena.h" 12 13 /* struct Pool is defined in pool.h so callers can size it (Compiler embeds 14 * a Pool* allocated through Heap, and core.c needs sizeof(Pool)). */ 15 16 #define POOL_INITIAL_TABLE_CAP 256 17 #define POOL_INITIAL_ENTRIES 64 18 #define POOL_TABLE_LOAD_NUM 3 19 #define POOL_TABLE_LOAD_DEN 4 /* grow when used*4 >= cap*3 */ 20 21 /* Cheap fold of (len + the first up-to-4 bytes) into a u32 cache key. This is 22 * deliberately NOT FNV: the whole point of the accelerator cache is to skip the 23 * full hash on repeats, so the key must be a couple of shifts/multiplies, not a 24 * per-byte hash. Collisions are harmless — every cache hit byte-verifies. */ 25 static u32 pool_icache_fold(const char* s, size_t len) { 26 u32 f = (u32)len * 0x9E3779B1u; 27 if (len > 0) f = (f ^ (u8)s[0]) * 0x01000193u; 28 if (len > 1) f = (f ^ (u8)s[1]) * 0x01000193u; 29 if (len > 2) f = (f ^ (u8)s[2]) * 0x01000193u; 30 if (len > 3) f = (f ^ (u8)s[3]) * 0x01000193u; 31 return f; 32 } 33 34 static u32 fnv1a(const char* s, size_t len) { 35 u32 h = 0x811C9DC5u; 36 size_t i; 37 for (i = 0; i < len; ++i) { 38 h ^= (u8)s[i]; 39 h *= 0x01000193u; 40 } 41 return h ? h : 1; /* avoid 0 (also reserved as "none") */ 42 } 43 44 static int sym_eq(const PoolEntry* e, const char* s, size_t len, u32 h) { 45 /* Inline byte loop instead of memcmp: the library is built -ffreestanding, 46 * under which clang emits a real memcmp libcall for a runtime length rather 47 * than inlining it. For the short identifiers the frontend interns, the call 48 * dispatch dominates the few-byte compare. The hash+len guards reject almost 49 * all non-matches before any byte is read. No word-at-a-time read: `s` points 50 * into the borrowed lexer buffer with no trailing-NUL/padding guarantee, so an 51 * over-wide load could overread at EOF; the byte loop is the safe form (and 52 * matches include/kit/core.h kit_slice_eq). */ 53 size_t i; 54 if (e->hash != h || e->len != (u32)len) return 0; 55 for (i = 0; i < len; ++i) 56 if (e->data[i] != s[i]) return 0; 57 return 1; 58 } 59 60 static void table_rehash(Pool* p, u32 new_cap) { 61 Sym* new_table = 62 (Sym*)p->heap->alloc(p->heap, sizeof(Sym) * new_cap, _Alignof(Sym)); 63 u32 i; 64 if (!new_table) return; 65 memset(new_table, 0, sizeof(Sym) * new_cap); 66 for (i = 0; i < p->cap; ++i) { 67 Sym sym = p->table[i]; 68 if (!sym) continue; 69 const PoolEntry* e = &p->entries[sym]; 70 u32 mask = new_cap - 1; 71 u32 j = e->hash & mask; 72 while (new_table[j]) j = (j + 1) & mask; 73 new_table[j] = sym; 74 } 75 if (p->table) p->heap->free(p->heap, p->table, sizeof(Sym) * p->cap); 76 p->table = new_table; 77 p->cap = new_cap; 78 /* Precompute the grow trigger so the per-intern load-factor test is a single 79 * compare (no multiplies). cap is always a power of two >= INITIAL_TABLE_CAP, 80 * so cap*LOAD_NUM is divisible by LOAD_DEN and this is exact: the trigger 81 * `used >= grow_threshold` fires at the same point as `used*DEN >= cap*NUM`. 82 * u64 in the product avoids overflow when cap is large. */ 83 p->grow_threshold = 84 (u32)((u64)new_cap * POOL_TABLE_LOAD_NUM / POOL_TABLE_LOAD_DEN); 85 } 86 87 static int entries_grow(Pool* p) { 88 u32 new_cap; 89 PoolEntry* ne; 90 if (p->nentries < p->entries_cap) return 0; 91 new_cap = p->entries_cap ? p->entries_cap * 2 : POOL_INITIAL_ENTRIES; 92 ne = (PoolEntry*)p->heap->realloc( 93 p->heap, p->entries, sizeof(*p->entries) * p->entries_cap, 94 sizeof(*p->entries) * new_cap, _Alignof(PoolEntry)); 95 if (!ne) return 1; 96 p->entries = ne; 97 p->entries_cap = new_cap; 98 return 0; 99 } 100 101 void pool_init(Pool* p, Heap* h) { 102 p->heap = h; 103 arena_init(&p->arena, h, 0); 104 p->table = NULL; 105 p->cap = 0; 106 p->used = 0; 107 p->entries = NULL; 108 p->nentries = 0; 109 p->entries_cap = 0; 110 /* Direct-mapped intern accelerator, zero-init (sym 0 = empty slot). If the 111 * allocation fails the cache stays NULL and intern simply runs the normal 112 * hash+probe path — it is a pure accelerator. */ 113 p->icache = (PoolICacheSlot*)h->alloc( 114 h, sizeof(PoolICacheSlot) * POOL_ICACHE_SIZE, _Alignof(PoolICacheSlot)); 115 if (p->icache) 116 memset(p->icache, 0, sizeof(PoolICacheSlot) * POOL_ICACHE_SIZE); 117 table_rehash(p, POOL_INITIAL_TABLE_CAP); 118 /* Reserve entry 0 as the "none" sentinel. */ 119 if (entries_grow(p) == 0) { 120 p->entries[0].data = NULL; 121 p->entries[0].len = 0; 122 p->entries[0].hash = 0; 123 p->nentries = 1; 124 } 125 } 126 127 void pool_fini(Pool* p) { 128 if (p->table) p->heap->free(p->heap, p->table, sizeof(Sym) * p->cap); 129 if (p->entries) 130 p->heap->free(p->heap, p->entries, sizeof(*p->entries) * p->entries_cap); 131 if (p->icache) 132 p->heap->free(p->heap, p->icache, 133 sizeof(PoolICacheSlot) * POOL_ICACHE_SIZE); 134 arena_fini(&p->arena); 135 p->table = NULL; 136 p->entries = NULL; 137 p->icache = NULL; 138 } 139 140 Sym pool_intern_cstr(Pool* p, const char* z) { 141 return pool_intern_slice(p, slice_from_cstr(z)); 142 } 143 144 Sym pool_intern_slice(Pool* p, Slice in) { 145 const char* s = in.s; 146 size_t len = in.len; 147 u32 h, mask, i; 148 u32 fold = 0, cidx = 0; 149 Sym sym; 150 151 if (!s || len == 0) return 0; 152 153 /* Direct-mapped accelerator: try to short-circuit BEFORE FNV+probe. On a hit 154 * we byte-verify against the interned bytes (same compare sym_eq does), so 155 * this can never return a wrong Sym. Misses/mismatches fall through to the 156 * normal path and then re-prime this slot from the result. */ 157 if (p->icache) { 158 fold = pool_icache_fold(s, len); 159 cidx = fold & (POOL_ICACHE_SIZE - 1); 160 { 161 const PoolICacheSlot* slot = &p->icache[cidx]; 162 if (slot->keyhash == fold && slot->sym != 0) { 163 const PoolEntry* e = &p->entries[slot->sym]; 164 if (e->len == (u32)len) { 165 size_t k = 0; 166 while (k < len && e->data[k] == s[k]) ++k; 167 if (k == len) return slot->sym; 168 } 169 } 170 } 171 } 172 173 if (p->used >= p->grow_threshold) { 174 table_rehash(p, p->cap * 2); 175 } 176 h = fnv1a(s, len); 177 mask = p->cap - 1; 178 i = h & mask; 179 /* Hoist the table/entries bases: they are stable across the probe (a grow 180 * only happens after a miss, below), so the compiler need not reload them 181 * from the Pool each iteration. This is the per-identifier intern probe. */ 182 { 183 const Sym* table = p->table; 184 const PoolEntry* ents = p->entries; 185 while ((sym = table[i]) != 0) { 186 if (sym_eq(&ents[sym], s, len, h)) { 187 /* Prime the accelerator so the next occurrence skips FNV+probe. */ 188 if (p->icache) { 189 p->icache[cidx].keyhash = fold; 190 p->icache[cidx].sym = sym; 191 } 192 return sym; 193 } 194 i = (i + 1) & mask; 195 } 196 } 197 /* Not found: allocate a new entry. The stored buffer carries a trailing 198 * NUL so a returned slice's pointer can be handed to a NUL-terminated 199 * boundary API; the recorded `len` is the logical length, exclusive of the 200 * terminator. The strtab content may carry embedded NULs, so exact-byte 201 * consumers compare via (len, memcmp). */ 202 if (entries_grow(p)) return 0; 203 { 204 char* dst = (char*)arena_alloc(&p->arena, len + 1, 1); 205 if (!dst) return 0; 206 /* Inline copy for the short-string common case: under -ffreestanding the 207 * memcpy is a libcall, and interned spellings are short (a few bytes), so 208 * the inline loop wins the call dispatch. Fall back to memcpy for the rare 209 * long spelling (e.g. a big string-literal token routed through intern). */ 210 if (len <= 32) { 211 size_t i; 212 for (i = 0; i < len; ++i) dst[i] = s[i]; 213 } else { 214 memcpy(dst, s, len); 215 } 216 dst[len] = '\0'; 217 sym = (Sym)p->nentries++; 218 p->entries[sym].data = dst; 219 p->entries[sym].len = (u32)len; 220 p->entries[sym].hash = h; 221 p->table[i] = sym; 222 p->used++; 223 } 224 /* Prime the accelerator with the freshly interned spelling. */ 225 if (p->icache) { 226 p->icache[cidx].keyhash = fold; 227 p->icache[cidx].sym = sym; 228 } 229 return sym; 230 } 231 232 Slice pool_slice(Pool* p, Sym sym) { 233 if (sym == 0 || sym >= p->nentries) return SLICE_NULL; 234 return (Slice){.s = p->entries[sym].data, .len = p->entries[sym].len}; 235 }