kit

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

commit b073810bd67ba34a5f4f213fa3899d6df6f03429
parent 8e8000ef0e4b077ea707cf17e46d23b2695af928
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Wed, 10 Jun 2026 21:31:58 -0700

perf(types): hash-cons structural types; drop O(n) derived-list scan

type_ptr/array/func and qualified non-tagged types were interned by a linear
scan of one flat per-pool derived list, which grows O(types) — so each
construction was O(types) and a type-population-heavy TU was O(n^2). Replace the
scan with an open-addressed structural intern table keyed on kind/qual + the
kind-specific identity fields (field-wise hash+eq, never a raw memcmp).

Tagged types (struct/union/enum) keep their existing declaration-identity list
and exact memcmp semantics — only the high-volume structural types move. Their
nodes are now arena-allocated without zeroing (the table compares fields, not
bytes), removing the per-node memset for the common case.

type-decl axis 1234ms -> 1078ms (-12.6%); object output byte-identical across the
toy/parse/derived-type corpora. Verified: test-cg-api/toy/parse/isa/opt/debug/
dwarf/smoke-x64 all green.

Diffstat:
Mlang/c/type/type.c | 306++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
1 file changed, 243 insertions(+), 63 deletions(-)

diff --git a/lang/c/type/type.c b/lang/c/type/type.c @@ -37,10 +37,30 @@ struct CgRecordMemo { KitCgTypeId id; }; +/* Open-addressed structural intern table for derived types whose identity is + * STRUCTURAL — pointer / array / function, and qualified variants of any + * non-tagged type. Slots hold the canonical Type*; lookup hashes and compares + * only the kind-specific identity fields (never a raw memcmp), so structural + * nodes need no zeroing and their padding is irrelevant. Tagged types + * (struct/union/enum) are interned by declaration identity instead and live on + * `derived`. Replaces the former O(n) linear scan of the whole derived list — + * that scan was O(types * derivations), catastrophic on type-population-heavy + * TUs. The table backing array is arena-allocated; old arrays leak into the + * arena and are reclaimed wholesale at pool teardown (no fini hook). */ +typedef struct TypeInternTable { + const Type** slots; /* cap entries, power-of-two; NULL == empty */ + u32 cap; + u32 used; +} TypeInternTable; + typedef struct PoolTypeCache { /* Direct slots for void + primitive kinds (TY_VOID..TY_LDOUBLE). */ const Type* prim[NUM_PRIM_KINDS]; - /* Linked list of every other type allocated through this pool. */ + /* Structurally-interned ptr/array/func + qualified non-tagged types. */ + TypeInternTable structural; + /* Identity-interned tagged types (struct/union/enum, qualified or not) and + * the throwaway record-builder results: a list, scanned only by the (rare) + * tag-based type_qualified / type_unqual paths and never per derivation. */ TypeListNode* derived; /* Completed record layout ids are compiler-local. */ CgRecordMemo* cg_record_memos; @@ -68,6 +88,115 @@ static Type* alloc_type_node(Pool* p, PoolTypeCache* c) { return &n->ty; } +/* Bare structural-type node: arena-allocated, NOT linked into `derived`, NOT + * zeroed. Each constructor writes exactly the fields its kind reads; the + * structural table compares those fields (not raw bytes), so the union tail and + * padding may stay uninitialized. */ +static Type* alloc_struct_type(Pool* p) { return arena_new(p->arena, Type); } + +static u32 type_fnv_ptr(u32 h, const void* q) { + u64 u = (u64)(uintptr_t)q; + h = (h ^ (u32)u) * 16777619u; + h = (h ^ (u32)(u >> 32)) * 16777619u; + return h; +} + +/* Structural hash over a (non-tagged) derived type's identity fields. */ +static u32 type_struct_hash(const Type* t) { + u32 h = 2166136261u; + h = (h ^ (u32)t->kind) * 16777619u; + h = (h ^ (u32)t->qual) * 16777619u; + switch (t->kind) { + case TY_PTR: + h = type_fnv_ptr(h, t->ptr.pointee); + break; + case TY_ARRAY: + h = type_fnv_ptr(h, t->arr.elem); + h = (h ^ t->arr.count) * 16777619u; + h = (h ^ (u32)t->arr.incomplete) * 16777619u; + break; + case TY_FUNC: { + u16 i; + h = type_fnv_ptr(h, t->fn.ret); + h = (h ^ (u32)t->fn.nparams) * 16777619u; + h = (h ^ (u32)t->fn.variadic) * 16777619u; + for (i = 0; i < t->fn.nparams; ++i) h = type_fnv_ptr(h, t->fn.params[i]); + break; + } + default: /* qualified primitive: kind + qual is the whole key */ + break; + } + return h; +} + +static int type_struct_eq(const Type* a, const Type* b) { + if (a->kind != b->kind || a->qual != b->qual) return 0; + switch (a->kind) { + case TY_PTR: + return a->ptr.pointee == b->ptr.pointee; + case TY_ARRAY: + return a->arr.elem == b->arr.elem && a->arr.count == b->arr.count && + a->arr.incomplete == b->arr.incomplete; + case TY_FUNC: { + u16 i; + if (a->fn.ret != b->fn.ret || a->fn.nparams != b->fn.nparams || + a->fn.variadic != b->fn.variadic) + return 0; + for (i = 0; i < a->fn.nparams; ++i) + if (a->fn.params[i] != b->fn.params[i]) return 0; + return 1; + } + default: /* qualified primitive: kind + qual already matched */ + return 1; + } +} + +static const Type* type_intern_find(const TypeInternTable* tab, + const Type* probe, u32 h) { + u32 mask, i; + const Type* s; + if (!tab->slots) return NULL; + mask = tab->cap - 1u; + i = h & mask; + while ((s = tab->slots[i]) != NULL) { + if (type_struct_eq(s, probe)) return s; + i = (i + 1u) & mask; + } + return NULL; +} + +/* Insert a freshly built canonical node. Best-effort under OOM: on a failed + * grow the node is simply not interned (a later identical query allocates a + * duplicate — correct, just unshared), matching the arena/buf failure policy. */ +static void type_intern_add(Pool* p, TypeInternTable* tab, const Type* t, + u32 h) { + u32 mask, i; + if (!tab->slots || (tab->used + 1u) * 4u >= tab->cap * 3u) { + u32 ncap = tab->cap ? tab->cap * 2u : 64u; + const Type** ns = arena_zarray(p->arena, const Type*, ncap); + if (!ns) return; + if (tab->slots) { + u32 nmask = ncap - 1u, k; + for (k = 0; k < tab->cap; ++k) { + const Type* s = tab->slots[k]; + u32 j; + if (!s) continue; + j = type_struct_hash(s) & nmask; + while (ns[j]) j = (j + 1u) & nmask; + ns[j] = s; + } + } + tab->slots = ns; + tab->cap = ncap; + } + if (!tab->slots) return; + mask = tab->cap - 1u; + i = h & mask; + while (tab->slots[i]) i = (i + 1u) & mask; + tab->slots[i] = t; + tab->used++; +} + const Type* type_void(Pool* p) { return type_prim(p, TY_VOID); } const Type* type_prim(Pool* p, TypeKind kind) { @@ -85,61 +214,72 @@ const Type* type_prim(Pool* p, TypeKind kind) { const Type* type_ptr(Pool* p, const Type* pointee) { PoolTypeCache* c = cache_get(p); + Type probe; + u32 h; + const Type* found; + Type* t; if (!c) return NULL; - /* Linear search; small N in practice. */ - for (TypeListNode* n = c->derived; n; n = n->next) { - if (n->ty.kind == TY_PTR && n->ty.qual == 0 && - n->ty.ptr.pointee == pointee) { - return &n->ty; - } - } - Type* t = alloc_type_node(p, c); + probe.kind = TY_PTR; + probe.qual = 0; + probe.ptr.pointee = pointee; + h = type_struct_hash(&probe); + found = type_intern_find(&c->structural, &probe, h); + if (found) return found; + t = alloc_struct_type(p); if (!t) return NULL; t->kind = TY_PTR; t->qual = 0; t->ptr.pointee = pointee; + type_intern_add(p, &c->structural, t, h); return t; } const Type* type_array(Pool* p, const Type* elem, u32 count, int incomplete) { PoolTypeCache* c = cache_get(p); + Type probe; + u32 h; + const Type* found; + Type* t; if (!c) return NULL; - for (TypeListNode* n = c->derived; n; n = n->next) { - if (n->ty.kind == TY_ARRAY && n->ty.qual == 0 && n->ty.arr.elem == elem && - n->ty.arr.count == count && - n->ty.arr.incomplete == (u8)(incomplete ? 1 : 0)) { - return &n->ty; - } - } - Type* t = alloc_type_node(p, c); + probe.kind = TY_ARRAY; + probe.qual = 0; + probe.arr.elem = elem; + probe.arr.count = count; + probe.arr.incomplete = (u8)(incomplete ? 1 : 0); + h = type_struct_hash(&probe); + found = type_intern_find(&c->structural, &probe, h); + if (found) return found; + t = alloc_struct_type(p); if (!t) return NULL; t->kind = TY_ARRAY; t->qual = 0; t->arr.elem = elem; t->arr.count = count; t->arr.incomplete = (u8)(incomplete ? 1 : 0); + type_intern_add(p, &c->structural, t, h); return t; } -static int param_arrays_eq(const Type* const* a, const Type* const* b, u16 n) { - for (u16 i = 0; i < n; ++i) - if (a[i] != b[i]) return 0; - return 1; -} - const Type* type_func(Pool* p, const Type* ret, const Type** params, u16 n, int variadic) { PoolTypeCache* c = cache_get(p); + Type probe; + u32 h; + const Type* found; + Type* t; if (!c) return NULL; - for (TypeListNode* nd = c->derived; nd; nd = nd->next) { - if (nd->ty.kind == TY_FUNC && nd->ty.qual == 0 && nd->ty.fn.ret == ret && - nd->ty.fn.nparams == n && - nd->ty.fn.variadic == (u8)(variadic ? 1 : 0) && - param_arrays_eq(nd->ty.fn.params, params, n)) { - return &nd->ty; - } - } - Type* t = alloc_type_node(p, c); + /* Borrow the caller's params array for the lookup; type_struct_eq compares it + * element-wise. Only on a miss do we allocate a persistent copy. */ + probe.kind = TY_FUNC; + probe.qual = 0; + probe.fn.ret = ret; + probe.fn.params = params; + probe.fn.nparams = n; + probe.fn.variadic = (u8)(variadic ? 1 : 0); + h = type_struct_hash(&probe); + found = type_intern_find(&c->structural, &probe, h); + if (found) return found; + t = alloc_struct_type(p); if (!t) return NULL; t->kind = TY_FUNC; t->qual = 0; @@ -154,6 +294,7 @@ const Type* type_func(Pool* p, const Type* ret, const Type** params, u16 n, } else { t->fn.params = NULL; } + type_intern_add(p, &c->structural, t, h); return t; } @@ -164,25 +305,44 @@ const Type* type_qualified(Pool* p, const Type* base, u16 qual) { if (!base || qual == 0) return base; c = cache_get(p); if (!c) return NULL; - /* Build the comparison template once, via assignment (not aggregate - * initialization). Struct assignment copies the full object representation, - * including padding bytes, so the memcmp below is byte-stable against the - * interned nodes (which are likewise produced by `*t = tmpl`). An aggregate - * *initializer* (`Type tmpl = *base;`) is lowered as a field-by-field copy - * that leaves padding unspecified, which would make this memcmp miss and - * intern a duplicate qualified type. See type_unqual for the same idiom. */ - tmpl = *base; - tmpl.qual = qual; - for (TypeListNode* n = c->derived; n; n = n->next) { - if (n->ty.kind == base->kind && n->ty.qual == qual && - memcmp(&n->ty, &tmpl, sizeof(Type)) == 0) { - return &n->ty; + if (base->kind == TY_STRUCT || base->kind == TY_UNION || + base->kind == TY_ENUM) { + /* Tagged types are identity-interned on `derived`. Build the comparison + * template via assignment (not aggregate initialization). Struct assignment + * copies the full object representation, including padding bytes, so the + * memcmp below is byte-stable against the interned nodes (which are likewise + * produced by `*t = tmpl`). An aggregate *initializer* (`Type tmpl = + * *base;`) is lowered as a field-by-field copy that leaves padding + * unspecified, which would make this memcmp miss and intern a duplicate. */ + tmpl = *base; + tmpl.qual = qual; + for (TypeListNode* n = c->derived; n; n = n->next) { + if (n->ty.kind == base->kind && n->ty.qual == qual && + memcmp(&n->ty, &tmpl, sizeof(Type)) == 0) { + return &n->ty; + } } + t = alloc_type_node(p, c); + if (!t) return NULL; + *t = tmpl; + return t; + } + /* Non-tagged: structurally interned (qualified prim / ptr / array / func). */ + { + Type probe = *base; + u32 h; + const Type* found; + probe.qual = qual; + h = type_struct_hash(&probe); + found = type_intern_find(&c->structural, &probe, h); + if (found) return found; + t = alloc_struct_type(p); + if (!t) return NULL; + *t = *base; + t->qual = qual; + type_intern_add(p, &c->structural, t, h); + return t; } - t = alloc_type_node(p, c); - if (!t) return NULL; - *t = tmpl; - return t; } /* ---- aggregates ---- */ @@ -306,24 +466,44 @@ const Type* type_unqual(Pool* p, const Type* t) { return type_prim(p, (TypeKind)t->kind); c = cache_get(p); if (!c) return NULL; - if ((t->kind == TY_STRUCT || t->kind == TY_UNION) && - t->rec.tag_id != TAG_NONE) { - for (TypeListNode* n = c->derived; n; n = n->next) { - if (n->ty.kind == t->kind && n->ty.qual == 0 && - n->ty.rec.tag_id == t->rec.tag_id && !n->ty.rec.incomplete) { - return &n->ty; + if (t->kind == TY_STRUCT || t->kind == TY_UNION || t->kind == TY_ENUM) { + /* Tagged types are identity-interned on `derived`. */ + if ((t->kind == TY_STRUCT || t->kind == TY_UNION) && + t->rec.tag_id != TAG_NONE) { + for (TypeListNode* n = c->derived; n; n = n->next) { + if (n->ty.kind == t->kind && n->ty.qual == 0 && + n->ty.rec.tag_id == t->rec.tag_id && !n->ty.rec.incomplete) { + return &n->ty; + } } } + tmpl = *t; + tmpl.qual = 0; + for (TypeListNode* n = c->derived; n; n = n->next) { + if (memcmp(&n->ty, &tmpl, sizeof(Type)) == 0) return &n->ty; + } + nt = alloc_type_node(p, c); + if (!nt) return NULL; + *nt = tmpl; + return nt; } - tmpl = *t; - tmpl.qual = 0; - for (TypeListNode* n = c->derived; n; n = n->next) { - if (memcmp(&n->ty, &tmpl, sizeof(Type)) == 0) return &n->ty; + /* Non-tagged: structurally interned (qualified prim is handled above as a + * prim; here that leaves ptr / array / func). */ + { + Type probe = *t; + u32 h; + const Type* found; + probe.qual = 0; + h = type_struct_hash(&probe); + found = type_intern_find(&c->structural, &probe, h); + if (found) return found; + nt = alloc_struct_type(p); + if (!nt) return NULL; + *nt = *t; + nt->qual = 0; + type_intern_add(p, &c->structural, nt, h); + return nt; } - nt = alloc_type_node(p, c); - if (!nt) return NULL; - *nt = tmpl; - return nt; } const Type* type_promoted(Pool* p, const Type* t) {