kit

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

commit dc4a6b86a9e69010438db1b6d5131b3432879320
parent 231ebcd6c7c16800a012415b42d1cabdf343d6bf
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Fri, 12 Jun 2026 11:00:39 -0700

perf(cg,abi): de-quadratic type-lowering caches (O(n^2)->O(1))

The derived-type dedup paths each scanned a growing table on every type use,
making type-heavy compilation O(n^2). On sqlite3.c (263k lines) this was ~70% of
frontend self-time, dominated by find_ptr_type_id.

- find_ptr_type_id / find_array_type_id: linear scan of all CG types -> packed
  u64-keyed hashmaps (CgPtrMap/CgArrayMap on CgApiState).
- find_func_type_id: linear scan -> structural hashset of CgApiType* (CgFuncSet);
  each entry now carries its self_id so the set can return the id. SEGVEC entries
  have stable addresses, so storing pointers is sound.
- abi_cg_func_info / abi_cg_record_layout: linearly-scanned linked-list caches ->
  type-id-keyed hashmaps (AbiFnInfoMap/AbiRecLayoutMap); values stay arena-backed.

Byte-identical object output on sqlite3.c. Whole-compile: 1.68x wall / 2.20x
cycles / 1.56x instructions vs before; the cycle win exceeds the instruction win
because the scans were cache-miss-heavy pointer chases.

Diffstat:
Msrc/abi/abi.c | 39++++++++++++++++++---------------------
Msrc/abi/abi_internal.h | 26+++++++++++---------------
Msrc/cg/type.c | 123+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
3 files changed, 115 insertions(+), 73 deletions(-)

diff --git a/src/abi/abi.c b/src/abi/abi.c @@ -135,17 +135,14 @@ static ABIRecordLayout* compute_record_layout(TargetABI* a, KitCgTypeId id) { const ABIRecordLayout* abi_cg_record_layout(TargetABI* a, KitCgTypeId id) { const CgType* t = cg_type_get(a->c, id); + ABIRecordLayout** hit; + ABIRecordLayout* L; if (!t || t->kind != KIT_CG_TYPE_RECORD) return NULL; - for (RecordLayoutCacheEntry* e = a->rec_cache; e; e = e->next) { - if (e->ty == id) return e->layout; - } - ABIRecordLayout* L = compute_record_layout(a, id); + hit = AbiRecLayoutMap_get(&a->rec_cache, id); + if (hit) return *hit; + L = compute_record_layout(a, id); if (!L) return NULL; - RecordLayoutCacheEntry* e = arena_new(a->c->tu, RecordLayoutCacheEntry); - e->ty = id; - e->layout = L; - e->next = a->rec_cache; - a->rec_cache = e; + AbiRecLayoutMap_set(&a->rec_cache, id, L); return L; } @@ -240,17 +237,14 @@ ABIFuncInfo* abi_compute_func_info_generic(TargetABI* a, KitCgTypeId fn, const ABIFuncInfo* abi_cg_func_info(TargetABI* a, KitCgTypeId fn_type) { const CgType* fn = cg_type_get(a->c, fn_type); + ABIFuncInfo** hit; + ABIFuncInfo* info; if (!fn || fn->kind != KIT_CG_TYPE_FUNC) return NULL; - for (FuncInfoCacheEntry* e = a->fn_cache; e; e = e->next) { - if (e->fn == fn_type) return e->info; - } - ABIFuncInfo* info = a->vt->compute_func_info(a, fn_type); + hit = AbiFnInfoMap_get(&a->fn_cache, fn_type); + if (hit) return *hit; + info = a->vt->compute_func_info(a, fn_type); if (!info) return NULL; - FuncInfoCacheEntry* e = arena_new(a->c->tu, FuncInfoCacheEntry); - e->fn = fn_type; - e->info = info; - e->next = a->fn_cache; - a->fn_cache = e; + AbiFnInfoMap_set(&a->fn_cache, fn_type, info); return info; } @@ -287,13 +281,16 @@ void abi_init(TargetABI* a, Compiler* c) { memset(a, 0, sizeof *a); a->c = c; a->vt = select_vtable(c); + /* The cached values stay on c->tu (per-TU arena); only the index lives on the + * compiler heap, freed in abi_fini. */ + AbiFnInfoMap_init(&a->fn_cache, (Heap*)c->ctx->heap); + AbiRecLayoutMap_init(&a->rec_cache, (Heap*)c->ctx->heap); } void abi_fini(TargetABI* a) { - /* Arena-backed; nothing to release. */ if (!a) return; - a->fn_cache = NULL; - a->rec_cache = NULL; + AbiFnInfoMap_fini(&a->fn_cache); + AbiRecLayoutMap_fini(&a->rec_cache); a->vt = NULL; a->c = NULL; } diff --git a/src/abi/abi_internal.h b/src/abi/abi_internal.h @@ -2,6 +2,7 @@ #define KIT_ABI_INTERNAL_H #include "abi/abi.h" +#include "core/hashmap.h" /* Internal: per-ABI dispatch table. * @@ -49,26 +50,21 @@ const ABIVtable* abi_vtable_lookup(KitArchKind arch, KitObjFmt obj); * TU can reach into the per-TU caches via TargetABI*. abi.c owns the * cache plumbing; the per-ABI TUs only allocate ABIFuncInfo / record * builders out of c->tu. */ -typedef struct FuncInfoCacheEntry FuncInfoCacheEntry; -typedef struct RecordLayoutCacheEntry RecordLayoutCacheEntry; -struct FuncInfoCacheEntry { - KitCgTypeId fn; - ABIFuncInfo* info; - FuncInfoCacheEntry* next; -}; - -struct RecordLayoutCacheEntry { - KitCgTypeId ty; - ABIRecordLayout* layout; - RecordLayoutCacheEntry* next; -}; +/* Per-(function/record) ABI-info caches, keyed by the CgTypeId. A type id is + * always nonzero for a real lookup (cg_type_get(0) fails before the cache), so + * 0 is a safe empty-slot sentinel. O(1) probe replaces what were linearly + * scanned linked lists — O(#distinct types) per query, i.e. O(n^2) over a + * type-heavy TU. The cached values stay arena-allocated (per-TU lifetime); only + * the index structure lives on the compiler heap and is freed at abi_fini. */ +KIT_HASHMAP_DEFINE(AbiFnInfoMap, u32, ABIFuncInfo*, hash_u32); +KIT_HASHMAP_DEFINE(AbiRecLayoutMap, u32, ABIRecordLayout*, hash_u32); struct TargetABI { Compiler* c; const ABIVtable* vt; - FuncInfoCacheEntry* fn_cache; - RecordLayoutCacheEntry* rec_cache; + AbiFnInfoMap fn_cache; + AbiRecLayoutMap rec_cache; }; /* Shared classifier primitives implemented in abi.c. The byte-identical diff --git a/src/cg/type.c b/src/cg/type.c @@ -1,5 +1,6 @@ #include "arch/arch.h" #include "cg/internal.h" +#include "core/hashmap.h" #include "obj/obj.h" typedef enum CgApiTypeKind { @@ -31,6 +32,9 @@ typedef struct CgApiType { * the former pad, so the entry size is unchanged. */ u8 cached_class; u8 pad[1]; + /* This entry's own type id (set by type_alloc). Lets a structural hashset of + * CgApiType* recover the id without maintaining a reverse index. */ + KitCgTypeId self_id; } CgApiType; /* High bit of CgApiType.cached_class: set once the class has been computed, so @@ -39,9 +43,38 @@ typedef struct CgApiType { SEGVEC_DEFINE(CgApiTypes, CgApiType, CG_API_TYPE_SEG_SHIFT); +/* Structural dedup of derived ptr/array types, O(1) instead of a linear scan of + * the whole type table on every derivation (which made each derived-type use + * O(#types) — the dominant frontend hotspot on type-heavy input like sqlite). + * The key packs the structural identity into a u64: ptr = (pointee<<32)|addr_sp, + * array = (elem<<32)|count. Both component high halves (pointee/elem) are real + * type ids, never KIT_CG_TYPE_NONE (0), so the packed key is always nonzero and + * 0 is a safe empty-slot sentinel. Value is the interned KitCgTypeId. */ +static inline u64 cg_ptr_key(KitCgTypeId pointee, u32 address_space) { + return ((u64)pointee << 32) | (u64)address_space; +} +static inline u64 cg_array_key(KitCgTypeId elem, u64 count) { + return ((u64)elem << 32) | (count & 0xffffffffu); +} +KIT_HASHMAP_DEFINE(CgPtrMap, u64, KitCgTypeId, hash_u64); +KIT_HASHMAP_DEFINE(CgArrayMap, u64, KitCgTypeId, hash_u64); + +/* Function types have a structural key (result + params + variadic + conv) that + * a scalar hashmap can't express, so dedup them through a structural hashset of + * the stored CgApiType* (SEGVEC entries have stable addresses). The callbacks + * are defined further down (after the shared result/param comparators); forward + * declarations let the set be defined here, before CgApiState embeds it. */ +static u32 cg_func_hash(CgApiType* const e); +static int cg_func_eq(CgApiType* const a, CgApiType* const b); +KIT_HASHSET_DEFINE(CgFuncSet, CgApiType*, cg_func_hash, cg_func_eq); + typedef struct CgApiState { Heap* heap; CgApiTypes types; + /* Structural dedup indexes for derived ptr/array/func types (see above). */ + CgPtrMap ptr_index; + CgArrayMap array_index; + CgFuncSet func_index; CgType builtins[KIT_CG_BUILTIN_COUNT]; /* Packed API_TYPE_CLASS_* per builtin, precomputed at init so the hottest * operand types (i32/i64/f64/ptr/...) classify with a single indexed load. */ @@ -78,10 +111,6 @@ static KitCgTypeId user_id_from_index(u32 index) { return type_id_from_tuple(raw_seg + CG_API_TYPE_USER_SEG_BIAS, off); } -static KitCgTypeId type_id_for_user_index(u32 index) { - return user_id_from_index(index); -} - static u64 cg_align_to(u64 n, u32 align) { u64 a = align ? (u64)align : 1u; return ((n + a - 1u) / a) * a; @@ -288,6 +317,9 @@ static CgApiState* cg_api_get(Compiler* c) { memset(s, 0, sizeof(*s)); s->heap = h; CgApiTypes_init(&s->types, h); + CgPtrMap_init(&s->ptr_index, h); + CgArrayMap_init(&s->array_index, h); + CgFuncSet_init(&s->func_index, h); c->cg_api = s; c->cg_api_free = cg_api_fini; cg_api_init_builtins(c, s); @@ -421,39 +453,28 @@ static CgApiType* type_alloc(Compiler* c, KitCgTypeId* id_out) { if (!e) return NULL; *id_out = user_id_from_index(index); if (*id_out == KIT_CG_TYPE_NONE) return NULL; + e->self_id = *id_out; return e; } static KitCgTypeId find_ptr_type_id(Compiler* c, KitCgTypeId pointee, u32 address_space) { CgApiState* s; - u32 n; + const KitCgTypeId* hit; if (!c || !c->cg_api) return KIT_CG_TYPE_NONE; s = (CgApiState*)c->cg_api; - n = CgApiTypes_count(&s->types); - for (u32 i = 0; i < n; ++i) { - CgApiType* e = CgApiTypes_at(&s->types, i); - if (e && e->kind == CG_API_TYPE_PTR && e->base == pointee && - e->address_space == address_space) - return type_id_for_user_index(i); - } - return KIT_CG_TYPE_NONE; + hit = CgPtrMap_get(&s->ptr_index, cg_ptr_key(pointee, address_space)); + return hit ? *hit : KIT_CG_TYPE_NONE; } static KitCgTypeId find_array_type_id(Compiler* c, KitCgTypeId elem, u64 count) { CgApiState* s; - u32 n; + const KitCgTypeId* hit; if (!c || !c->cg_api) return KIT_CG_TYPE_NONE; s = (CgApiState*)c->cg_api; - n = CgApiTypes_count(&s->types); - for (u32 i = 0; i < n; ++i) { - CgApiType* e = CgApiTypes_at(&s->types, i); - if (e && e->kind == CG_API_TYPE_ARRAY && e->base == elem && - e->array_count == count) - return type_id_for_user_index(i); - } - return KIT_CG_TYPE_NONE; + hit = CgArrayMap_get(&s->array_index, cg_array_key(elem, count)); + return hit ? *hit : KIT_CG_TYPE_NONE; } static int cg_params_eq(const KitCgFuncParam* a, const KitCgFuncParam* b, @@ -471,25 +492,45 @@ static int cg_result_eq(const KitCgFuncResult* a, const KitCgFuncResult* b) { memcmp(&a->attrs, &b->attrs, sizeof(a->attrs)) == 0; } +/* Structural hash/eq over a function type's identity — the same fields the old + * linear scan compared. Only CG_API_TYPE_FUNC entries enter the set, so neither + * callback re-checks kind. The result/param attrs are compared (not hashed): + * fewer hash inputs just means eq resolves the (rare) collision. */ +static u32 cg_func_hash(CgApiType* const e) { + u32 h = 2166136261u; + h = (h ^ (u32)e->count) * 16777619u; + h = (h ^ (u32)e->abi_variadic) * 16777619u; + h = (h ^ (u32)e->call_conv) * 16777619u; + h = (h ^ (u32)e->result.type) * 16777619u; + for (u32 i = 0; i < e->count; ++i) + h = (h ^ (u32)e->params[i].type) * 16777619u; + return h; +} + +static int cg_func_eq(CgApiType* const a, CgApiType* const b) { + if (a->count != b->count) return 0; + if (a->abi_variadic != b->abi_variadic) return 0; + if (a->call_conv != b->call_conv) return 0; + if (!cg_result_eq(&a->result, &b->result)) return 0; + if (a->count && !cg_params_eq(a->params, b->params, a->count)) return 0; + return 1; +} + static KitCgTypeId find_func_type_id(Compiler* c, KitCgFuncSig sig) { CgApiState* s; - u32 n; + CgApiType probe; + CgApiType* hit; if (!c || !c->cg_api) return KIT_CG_TYPE_NONE; s = (CgApiState*)c->cg_api; - n = CgApiTypes_count(&s->types); - for (u32 i = 0; i < n; ++i) { - CgApiType* e = CgApiTypes_at(&s->types, i); - if (!e || e->kind != CG_API_TYPE_FUNC) continue; - if (e->count != sig.nparams) continue; - if (e->abi_variadic != (sig.abi_variadic != 0)) continue; - if (e->call_conv != sig.call_conv) continue; - if (!cg_result_eq(&e->result, &sig.result)) continue; - if (sig.nparams && !cg_params_eq(e->params, sig.params, sig.nparams)) { - continue; - } - return type_id_for_user_index(i); - } - return KIT_CG_TYPE_NONE; + memset(&probe, 0, sizeof probe); + probe.kind = CG_API_TYPE_FUNC; + probe.count = sig.nparams; + probe.abi_variadic = (u8)(sig.abi_variadic != 0); + probe.call_conv = sig.call_conv; + probe.result = sig.result; + probe.params = sig.params; + hit = CgFuncSet_find(&s->func_index, &probe); + return hit ? hit->self_id : KIT_CG_TYPE_NONE; } static CgApiType* api_type_from_id(Compiler* c, KitCgTypeId id) { @@ -767,6 +808,8 @@ KitCgTypeId kit_cg_type_ptr(KitCompiler* c, KitCgTypeId pointee, if (!cg_type_set_ptr(c, e, pointee, address_space)) { return KIT_CG_TYPE_NONE; } + CgPtrMap_set(&((CgApiState*)c->cg_api)->ptr_index, + cg_ptr_key(pointee, address_space), id); return id; } @@ -785,6 +828,8 @@ KitCgTypeId kit_cg_type_array(KitCompiler* c, KitCgTypeId elem, if (!cg_type_set_array(c, e, elem, count)) { return KIT_CG_TYPE_NONE; } + CgArrayMap_set(&((CgApiState*)c->cg_api)->array_index, + cg_array_key(elem, count), id); return id; } @@ -895,6 +940,7 @@ KitCgTypeId kit_cg_type_func(KitCompiler* c, KitCgFuncSig sig) { if (!cg_type_set_func(c, e, sig, copied)) { return KIT_CG_TYPE_NONE; } + CgFuncSet_add(&((CgApiState*)c->cg_api)->func_index, e); return id; } @@ -1109,6 +1155,9 @@ void cg_api_fini(Compiler* c) { if (!c || !c->cg_api) return; s = (CgApiState*)c->cg_api; CgApiTypes_fini(&s->types); + CgPtrMap_fini(&s->ptr_index); + CgArrayMap_fini(&s->array_index); + CgFuncSet_fini(&s->func_index); s->heap->free(s->heap, s, sizeof(*s)); c->cg_api = NULL; c->cg_api_free = NULL;