kit

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

commit ecbd00dc995aefbe600af5bfd5494412f7a59245
parent c80d7eb9de3126085252647b5738adf8977cc63b
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Sat, 13 Jun 2026 01:35:40 -0700

perf(cg): -O0 transient liveness + lazy dup (copy-on-write)

Foundation for value-stack-driven copy/slot coalescing at -O0. The value stack is
a precise liveness oracle: a transient (api_alloc_temp_local) local is dead iff no
live stack entry references it. KitCg now keeps a per-handle live-reference count
(local_refs[], maintained at api_push/api_pop, with pointer-range-guarded
reseat helpers for in-place operand changes) stamped by a per-function generation
(local_temp_gen[] vs func_gen, so stale entries need no O(cap) clear). api_temp_dead
uses the count only as a fast reject and CONFIRMS with a bounded stack scan
(api_temp_scan_refs) when it reads 0 — so any reseat-accounting gap costs at most a
missed optimization, never a miscompile. The scan is also the bounded-scan A/B
baseline; KIT_NO_COALESCE disables the whole thing for measurement.

First consumer — lazy dup: kit_cg_dup of an already-materialized owned temp now
pushes a second reference to the same write-once temp instead of copying it into a
fresh one. Both entries only read the shared temp; the refcount (now 2) keeps the
upcoming finer-reclaim/adoption from recycling it while shared, and it is freed
once both references are gone (Fix A at the statement boundary). Eliminates the
common assignment-result dup whose value is stored once and discarded
(`a=b;`, `x=y=z`, `d=(arr[i]=v)`).

sqlite3.c -c, arm64-macOS: 565,388 -> 543,470 insns (-3.9%); reg-reg mov
60,684 -> 45,770 (-24.6%); object 2.66 -> 2.57MB. Running total this session:
831,078 -> 543,470 (-34.6%), ~1.59x tcc.

test-toy 1392/0, test-parse-ok 3920/0 (O0/O1 x D/R/J/E), test-parse-err 129/0,
test-smoke-x64/rv64 3/0; sqlite shell links+runs (84|2); s.a++ + ++s.b == 3 and
chained/nested assignment cases verified.

Diffstat:
Msrc/cg/internal.h | 29+++++++++++++++++++++++++++++
Msrc/cg/memory.c | 13+++++++++++++
Msrc/cg/session.c | 3+++
Msrc/cg/value.c | 141++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
4 files changed, 185 insertions(+), 1 deletion(-)

diff --git a/src/cg/internal.h b/src/cg/internal.h @@ -205,6 +205,23 @@ struct KitCg { u32 sp; u32 cap; + /* -O0 transient liveness. local_refs[h] = number of live value-stack entries + * that reference CGLocal handle h (maintained at api_push/api_pop, plus a + * pointer-range-guarded reseat at in-place operand changes). local_is_temp[h] + * marks an api_alloc_temp_local transient — the only locals the single-pass + * backend recycles (Fix A) or coalesces. The count is used only as a + * fast-reject prefilter: a transient is treated as dead solely when the count + * reads 0 AND a confirming stack scan (api_temp_dead) agrees, so any reseat + * gap costs at most a missed optimization, never a miscompile. Sized on demand + * by handle; cleared per function. NULL/0 until first use. */ + u32* local_refs; /* live value-stack reference count per handle */ + u32* local_temp_gen; /* == func_gen iff the handle is this function's temp */ + u32 local_track_cap; + u32 func_gen; /* bumped each function; stamps temps without O(cap) clears */ + u8 coalesce; /* -O0 copy/dup coalescing + finer reclaim enabled */ + u8 coalesce_known; /* coalesce resolved from env (once) */ + u8 coalesce_pad[2]; + /* Off-node pool for SV_CMP / SV_ARITH delayed payloads. The arena is reset * per function (api_delayed_reset at func_begin, where the stack is also * dropped); delayed_free is an intrusive freelist that reuses payloads within @@ -509,6 +526,18 @@ void api_ensure_local(KitCg* g, ApiSValue* sv); Operand api_force_local(KitCg* g, ApiSValue* v, KitCgTypeId ty); Operand api_force_local_unless_imm(KitCg* g, ApiSValue* v, KitCgTypeId ty); void api_release(KitCg* g, ApiSValue* sv); + +/* -O0 transient liveness (see KitCg.local_refs). api_coalesce_on returns whether + * the copy/dup coalescing + finer-reclaim mechanisms are enabled this run. + * api_temp_dead reports whether transient `local` is provably dead right now (no + * live value-stack entry references it): count==0 confirmed by a stack scan. + * api_reseat_{begin,end} bracket an in-place change to a stack entry's operand so + * its references are re-accounted. */ +int api_coalesce_on(KitCg* g); +int api_temp_dead(KitCg* g, CGLocal local); +void api_reseat_begin(KitCg* g, const ApiSValue* sv); +void api_reseat_end(KitCg* g, const ApiSValue* sv); + BinOp api_map_int_binop(KitCgIntBinOp op); BinOp api_map_fp_binop(KitCgFpBinOp op); UnOp api_map_int_unop(KitCgIntUnOp op); diff --git a/src/cg/memory.c b/src/cg/memory.c @@ -577,6 +577,19 @@ void kit_cg_dup(KitCg* g) { api_push(g, v); return; } + if (api_coalesce_on(g)) { + /* Lazy dup: push a second reference to the owned temp instead of copying it + * into a fresh one. Both entries only ever read the write-once value temp, + * so they can share it; the value-stack refcount (now 2) keeps finer-reclaim + * and copy-adoption from recycling the temp while it is still shared, and it + * is freed once both references are gone (Fix A at the statement boundary, or + * finer reclaim when the count returns to 0). Eliminates the common + * assignment-result dup whose value is stored once and then discarded. */ + dup = v; + dup.pinned = 0; + api_push(g, dup); + return; + } top->pinned = 1; ty = api_owned_local_type(g, &v); r = api_alloc_temp_local(g, ty); diff --git a/src/cg/session.c b/src/cg/session.c @@ -411,6 +411,9 @@ void kit_cg_func_begin_attrs(KitCg* g, KitCgSym cg_sym, g->nlocals = 0; g->const_head = KIT_CG_LOCAL_NONE; g->sp = 0; + /* New generation: stale local_temp_gen entries from the previous function are + * now != func_gen, so its temp handles read as non-temp without any clear. */ + g->func_gen++; /* Drop the previous function's delayed cmp/arith payloads along with its * value stack — neither outlives the function. */ api_delayed_reset(g); diff --git a/src/cg/value.c b/src/cg/value.c @@ -154,6 +154,10 @@ void api_stack_grow(KitCg* g, u32 want) { g->cap = cap; } +/* -O0 transient liveness bookkeeping; defined below, used by push/pop/alloc. */ +static void api_sv_adjust_refs(KitCg* g, const ApiSValue* sv, int delta); +static void api_track_temp(KitCg* g, CGLocal local); + void api_push(KitCg* g, ApiSValue v) { KitCgTypeId ty = api_sv_type(&v); /* One memoized fetch gives both the aggregate-place property and the @@ -177,13 +181,17 @@ void api_push(KitCg* g, ApiSValue v) { v.bitfield.wide_kind = cls & API_TYPE_CLASS_WIDE_MASK; api_stack_grow(g, g->sp + 1); g->stack[g->sp++] = v; + api_sv_adjust_refs(g, &v, +1); } ApiSValue api_pop(KitCg* g) { + ApiSValue r; if (g->sp == 0) { compiler_panic(g->c, g->cur_loc, "KitCg: stack underflow"); } - return g->stack[--g->sp]; + r = g->stack[--g->sp]; + api_sv_adjust_refs(g, &r, -1); + return r; } /* ---- local helpers ---- */ @@ -237,6 +245,7 @@ CGLocal api_alloc_temp_local(KitCg* g, KitCgTypeId ty) { compiler_panic(g->c, g->cur_loc, "KitCg: target failed to allocate temporary local"); } + api_track_temp(g, local); /* register for -O0 transient liveness */ return local; } @@ -435,6 +444,136 @@ void api_release(KitCg* g, ApiSValue* sv) { sv->res = RES_INHERENT; } +/* ---- -O0 transient liveness (refcount + confirming scan) ---- */ + +int api_coalesce_on(KitCg* g) { + if (!g->coalesce_known) { + /* Default on; KIT_NO_COALESCE=1 disables for A/B measurement. */ + g->coalesce = kit_debug_getenv("KIT_NO_COALESCE") ? 0u : 1u; + g->coalesce_known = 1u; + } + return g->coalesce; +} + +/* Mark `local` (a fresh api_alloc_temp_local handle) as this function's temp and + * zero its live-reference count. */ +static void api_track_temp(KitCg* g, CGLocal local) { + Heap* h = g->c->ctx->heap; + u32 idx = (u32)local; + if (idx >= g->local_track_cap) { + u32 cap = g->local_track_cap ? g->local_track_cap : 64u; + u32* nr; + u32* ng; + while (cap <= idx) cap *= 2u; + nr = (u32*)h->alloc(h, sizeof(u32) * cap, _Alignof(u32)); + ng = (u32*)h->alloc(h, sizeof(u32) * cap, _Alignof(u32)); + memset(nr, 0, sizeof(u32) * cap); + memset(ng, 0, sizeof(u32) * cap); + if (g->local_refs) { + memcpy(nr, g->local_refs, sizeof(u32) * g->local_track_cap); + memcpy(ng, g->local_temp_gen, sizeof(u32) * g->local_track_cap); + h->free(h, g->local_refs, sizeof(u32) * g->local_track_cap); + h->free(h, g->local_temp_gen, sizeof(u32) * g->local_track_cap); + } + g->local_refs = nr; + g->local_temp_gen = ng; + g->local_track_cap = cap; + } + g->local_temp_gen[idx] = g->func_gen; + g->local_refs[idx] = 0u; +} + +static int api_handle_is_temp(KitCg* g, CGLocal local) { + u32 idx = (u32)local; + return idx < g->local_track_cap && g->local_temp_gen[idx] == g->func_gen; +} + +/* Visit each temp local an operand references (LOCAL, or INDIRECT base/index), + * adjusting its live-reference count by `delta` (clamped at 0 on decrement). */ +static void api_op_adjust_refs(KitCg* g, const Operand* op, int delta) { + CGLocal a = CG_LOCAL_NONE, b = CG_LOCAL_NONE; + if (op->kind == OPK_LOCAL) { + a = op->v.local; + } else if (op->kind == OPK_INDIRECT) { + a = op->v.ind.base; + b = op->v.ind.index; + } + if (a != CG_LOCAL_NONE && api_handle_is_temp(g, a)) { + if (delta > 0) + g->local_refs[a]++; + else if (g->local_refs[a]) + g->local_refs[a]--; + } + if (b != CG_LOCAL_NONE && api_handle_is_temp(g, b)) { + if (delta > 0) + g->local_refs[b]++; + else if (g->local_refs[b]) + g->local_refs[b]--; + } +} + +/* All temp locals an SValue references: its operand plus, for a delayed + * cmp/arith, its pending operands. */ +static void api_sv_adjust_refs(KitCg* g, const ApiSValue* sv, int delta) { + if (sv->kind == SV_OPERAND) { + api_op_adjust_refs(g, &sv->op, delta); + } else if (sv->delayed) { + if (sv->kind == SV_CMP) { + api_op_adjust_refs(g, &sv->delayed->cmp.a, delta); + api_op_adjust_refs(g, &sv->delayed->cmp.b, delta); + } else if (sv->kind == SV_ARITH) { + api_op_adjust_refs(g, &sv->delayed->arith.a, delta); + if (sv->delayed->arith.kind == API_DELAYED_BINOP) + api_op_adjust_refs(g, &sv->delayed->arith.b, delta); + } + } +} + +void api_reseat_begin(KitCg* g, const ApiSValue* sv) { + if (sv >= g->stack && sv < g->stack + g->sp) api_sv_adjust_refs(g, sv, -1); +} +void api_reseat_end(KitCg* g, const ApiSValue* sv) { + if (sv >= g->stack && sv < g->stack + g->sp) api_sv_adjust_refs(g, sv, +1); +} + +/* Ground-truth scan: how many live value-stack entries reference temp `local`. + * The arbiter when the incremental count reads 0 (so a reseat-accounting gap can + * only cost a missed optimization, never a miscompile). */ +static u32 api_temp_scan_refs(KitCg* g, CGLocal local) { + u32 n = 0; + for (u32 i = 0; i < g->sp; ++i) { + const ApiSValue* sv = &g->stack[i]; + const Operand* op = &sv->op; + if (sv->kind == SV_OPERAND) { + if (op->kind == OPK_LOCAL && op->v.local == local) n++; + else if (op->kind == OPK_INDIRECT && + (op->v.ind.base == local || op->v.ind.index == local)) + n++; + } else if (sv->delayed) { + const ApiDelayed* d = sv->delayed; + const Operand* a = sv->kind == SV_CMP ? &d->cmp.a : &d->arith.a; + const Operand* b = sv->kind == SV_CMP ? &d->cmp.b : &d->arith.b; + int has_b = sv->kind == SV_CMP || d->arith.kind == API_DELAYED_BINOP; + if ((a->kind == OPK_LOCAL && a->v.local == local) || + (a->kind == OPK_INDIRECT && + (a->v.ind.base == local || a->v.ind.index == local))) + n++; + if (has_b && + ((b->kind == OPK_LOCAL && b->v.local == local) || + (b->kind == OPK_INDIRECT && + (b->v.ind.base == local || b->v.ind.index == local)))) + n++; + } + } + return n; +} + +int api_temp_dead(KitCg* g, CGLocal local) { + if (local == CG_LOCAL_NONE || !api_handle_is_temp(g, local)) return 0; + if (g->local_refs[(u32)local] > 0) return 0; /* fast reject: still referenced */ + return api_temp_scan_refs(g, local) == 0; /* confirm before acting */ +} + /* ---- BinOp / UnOp / CmpOp mapping ---- */ BinOp api_map_int_binop(KitCgIntBinOp op) {