kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

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 }