commit bed853f9f7ddfc1e2dfa97e23198932522268b6c
parent f2841b12dee3f171dfab2888e0ebc619060a50b3
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Thu, 11 Jun 2026 16:02:39 -0700
perf(cg): memoize per-type value-stack classification + designated-init nodes
Two byte-identical constant-factor wins on the value-stack frontend (above the
CgTarget seam), gated on scripts/perf_identity_gate.sh (60-category, all PASS):
(b) Per-type classification memo. R6 cached the wide_kind result on each value
node, but api_push still re-derived it on every operand push — a string of
cg_type_get + alias-chase + ABI round trips (api_wide_kind_for) plus a
cg_type_is_aggregate query. Both are pure functions of (type, target), so
they are now computed once per type id and packed into one byte:
builtins precomputed at cg_api init (i32/i64/f64/ptr classify with a single
indexed load), user types filled lazily in the reclaimed CgApiType pad.
api_push reads the aggregate-place bit and the wide/soft class from it.
(c) Designated-init value nodes. The api_op_* / api_make_* constructors drop
their memset of the 112-byte ApiSValue for a designated compound literal, so
the compiler emits only live-field stores and elides immediately-overwritten
ones (same idiom as the NativeLoc constructors).
Measured (paired best-of vs the pre-change binary): body-size +3.5%,
ref-density +2.4%, locals-per-fn +2.3%, fn-count +1.6%. (b) is the bulk; (c)
~+0.75% (the delayed-union zeroing survives -ftrivial-auto-var-init=zero, so
(c) only reclaims the memset call). Gate PASS; test-cg-api/opt/toy and
test-smoke-x64 green. PERF.md Round-7 note added.
Diffstat:
5 files changed, 153 insertions(+), 97 deletions(-)
diff --git a/doc/plan/PERF.md b/doc/plan/PERF.md
@@ -106,6 +106,25 @@ change: only the UUID/build-id bytes move). New tooling:
`scripts/perf_identity_gate.sh` (60-category byte-identical gate) and
`scripts/perf_axis_time.py` (focused A/B axis timer).
+**Round 7 (value-stack frontend, byte-identical).** Two further constant-factor
+wins on the value-stack layer that sits *above* the `CgTarget` seam, gated on the
+same 60-category identity gate: **(b) per-type classification memo** — R6 cached
+the `wide_kind` result on each node but `api_push` still re-derived it (a string of
+`cg_type_get` + alias-chase + ABI round trips) on every operand; the wide/soft
+class and the aggregate-place bit are now computed once per type id (builtins
+precomputed at `cg_api` init, user types filled lazily in the `CgApiType` slack)
+and `api_push` reads both from one packed byte. **(c) designated-init value nodes**
+— the `api_op_*` / `api_make_*` constructors drop their `memset` of the (112-byte)
+`ApiSValue` for a compound literal, so the compiler stores only live fields and
+elides immediately-overwritten ones. Measured (paired vs the Round-6 binary):
+body-size **+3.5%**, ref-density **+2.4%**, locals-per-fn **+2.3%**, fn-count
+**+1.6%**; (b) is the bulk, (c) ~+0.75% (the delayed-union zeroing survives under
+`-ftrivial-auto-var-init=zero`, so (c) only reclaims the memset *call*). The
+node-shrink (move the 64-byte delayed cmp/arith union off the hot node) was
+assessed and **deferred**: the `-O0` fold machinery relies on by-value deep-copy of
+the delayed payload (`*out = *a` then divergent clears in `fold.c`), which a shared
+pointer would break — a larger, riskier change than the ~1–3% it would add.
+
The structural bet is fully in place: a single-pass no-AST C frontend, single-pass
code emission with patch-ups, and a format-neutral linker. The scaling bugs are
gone (no axis is superlinear) and the big constant factors with them — per-unit
diff --git a/src/cg/fold.c b/src/cg/fold.c
@@ -97,18 +97,15 @@ int api_try_fold_int_cmp(KitCg* g, CmpOp op, KitCgTypeId ty, i64 a, i64 b,
ApiSValue api_make_cmp(CmpOp op, Operand a, Operand b, KitCgTypeId result_ty,
int a_owned, int b_owned) {
- ApiSValue sv;
- memset(&sv, 0, sizeof sv);
- sv.kind = SV_CMP;
- sv.type = result_ty;
- sv.delayed.cmp.op = op;
- sv.delayed.cmp.a = a;
- sv.delayed.cmp.b = b;
- sv.delayed.cmp.a_owned = a_owned ? 1u : 0u;
- sv.delayed.cmp.b_owned = b_owned ? 1u : 0u;
- sv.res = RES_INHERENT;
- sv.source_local = KIT_CG_LOCAL_NONE;
- return sv;
+ return (ApiSValue){.kind = SV_CMP,
+ .type = result_ty,
+ .res = RES_INHERENT,
+ .source_local = KIT_CG_LOCAL_NONE,
+ .delayed.cmp = {.op = op,
+ .a = a,
+ .b = b,
+ .a_owned = a_owned ? 1u : 0u,
+ .b_owned = b_owned ? 1u : 0u}};
}
CmpOp api_invert_cmp(CmpOp op) {
@@ -199,34 +196,28 @@ void api_materialize_cmp_to(KitCg* g, ApiSValue* sv, Operand dst) {
* ============================================================ */
ApiSValue api_make_arith_unop(UnOp op, Operand a, KitCgTypeId ty, int a_owned) {
- ApiSValue sv;
- memset(&sv, 0, sizeof sv);
- sv.kind = SV_ARITH;
- sv.delayed.arith.kind = API_DELAYED_UNOP;
- sv.type = ty;
- sv.delayed.arith.un_op = op;
- sv.delayed.arith.a = a;
- sv.delayed.arith.a_owned = a_owned ? 1u : 0u;
- sv.res = RES_INHERENT;
- sv.source_local = KIT_CG_LOCAL_NONE;
- return sv;
+ return (ApiSValue){.kind = SV_ARITH,
+ .type = ty,
+ .res = RES_INHERENT,
+ .source_local = KIT_CG_LOCAL_NONE,
+ .delayed.arith = {.kind = API_DELAYED_UNOP,
+ .un_op = op,
+ .a = a,
+ .a_owned = a_owned ? 1u : 0u}};
}
ApiSValue api_make_arith_binop(BinOp op, Operand a, Operand b, KitCgTypeId ty,
int a_owned, int b_owned) {
- ApiSValue sv;
- memset(&sv, 0, sizeof sv);
- sv.kind = SV_ARITH;
- sv.delayed.arith.kind = API_DELAYED_BINOP;
- sv.type = ty;
- sv.delayed.arith.bin_op = op;
- sv.delayed.arith.a = a;
- sv.delayed.arith.b = b;
- sv.delayed.arith.a_owned = a_owned ? 1u : 0u;
- sv.delayed.arith.b_owned = b_owned ? 1u : 0u;
- sv.res = RES_INHERENT;
- sv.source_local = KIT_CG_LOCAL_NONE;
- return sv;
+ return (ApiSValue){.kind = SV_ARITH,
+ .type = ty,
+ .res = RES_INHERENT,
+ .source_local = KIT_CG_LOCAL_NONE,
+ .delayed.arith = {.kind = API_DELAYED_BINOP,
+ .bin_op = op,
+ .a = a,
+ .b = b,
+ .a_owned = a_owned ? 1u : 0u,
+ .b_owned = b_owned ? 1u : 0u}};
}
void api_release_arith(KitCg* g, ApiSValue* sv) {
diff --git a/src/cg/internal.h b/src/cg/internal.h
@@ -78,6 +78,14 @@ typedef enum ApiWideKind {
WK_SOFT_SINGLE, /* f32 on a pure-soft target (no FP unit) */
} ApiWideKind;
+/* Packed per-type classification memoized for the value stack. The wide/soft
+ * class and the aggregate-place property are both pure functions of (type,
+ * target), so they are computed once per type id (builtins precomputed at cg_api
+ * init, user types filled lazily) instead of re-derived on every api_push. The
+ * low bits hold the ApiWideKind; bit 3 marks an aggregate (record) type. */
+#define API_TYPE_CLASS_WIDE_MASK 0x07u
+#define API_TYPE_CLASS_AGGREGATE 0x08u
+
/* Bit-field geometry carried by a bit-field PLACE. `kit_cg_field` fills this
* from the record layout when it projects to a bit-field; a plain load/store on
* the carrying place then performs the extract/insert. The storage MemAccess is
@@ -422,7 +430,9 @@ int api_is_f128_type(Compiler* c, KitCgTypeId ty);
int api_is_i128_type(Compiler* c, KitCgTypeId ty);
int api_is_wide16_scalar_type(Compiler* c, KitCgTypeId ty);
int api_is_wide8_scalar_type(Compiler* c, KitCgTypeId ty);
-u8 api_wide_kind_for(KitCg* g, KitCgTypeId ty);
+u8 api_wide_kind_for(Compiler* c, KitCgTypeId ty);
+/* Memoized fetch of the packed API_TYPE_CLASS_* byte for `ty` (see above). */
+u8 api_type_class(KitCg* g, KitCgTypeId ty);
Operand api_op_imm(i64 v, KitCgTypeId ty);
Operand api_op_local(CGLocal r, KitCgTypeId ty);
Operand api_op_global(ObjSymId sym, i64 addend, KitCgTypeId ty);
diff --git a/src/cg/type.c b/src/cg/type.c
@@ -26,15 +26,26 @@ typedef struct CgApiType {
KitCgCallConv call_conv;
u8 kind;
u8 abi_variadic;
- u8 pad[2];
+ /* Lazily-filled packed API_TYPE_CLASS_* memo (0 == not yet computed; the
+ * VALID bit distinguishes a computed all-zero class from unfilled). Reuses
+ * the former pad, so the entry size is unchanged. */
+ u8 cached_class;
+ u8 pad[1];
} CgApiType;
+/* High bit of CgApiType.cached_class: set once the class has been computed, so
+ * a genuine {WK_NARROW, non-aggregate} (packed 0) is told apart from unfilled. */
+#define API_TYPE_CLASS_CACHED 0x80u
+
SEGVEC_DEFINE(CgApiTypes, CgApiType, CG_API_TYPE_SEG_SHIFT);
typedef struct CgApiState {
Heap* heap;
CgApiTypes types;
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. */
+ u8 builtin_class[KIT_CG_BUILTIN_COUNT];
u8 builtins_init;
u8 pad[3];
} CgApiState;
@@ -149,14 +160,54 @@ static void builtin_cg_type_init(Compiler* c, CgType* out, KitCgBuiltinType t) {
}
}
+static CgApiState* cg_api_get(Compiler* c);
+static CgApiType* api_type_from_id(Compiler* c, KitCgTypeId id);
+
+/* Compute the packed API_TYPE_CLASS_* byte for `ty` from scratch: the wide /
+ * soft-float dispatch class plus the aggregate-place bit. Both are pure
+ * functions of (type, target), so callers memoize the result (per builtin at
+ * init, per user type lazily) rather than re-run this on every value push. */
+static u8 api_compute_type_class(Compiler* c, KitCgTypeId ty) {
+ u8 cls = api_wide_kind_for(c, ty) & API_TYPE_CLASS_WIDE_MASK;
+ if (cg_type_is_aggregate(c, ty)) cls |= API_TYPE_CLASS_AGGREGATE;
+ return cls;
+}
+
static void cg_api_init_builtins(Compiler* c, CgApiState* s) {
if (s->builtins_init) return;
for (u32 i = 0; i < KIT_CG_BUILTIN_COUNT; ++i) {
builtin_cg_type_init(c, &s->builtins[i], (KitCgBuiltinType)i);
}
+ /* Precompute each builtin's class now that the builtin table and the ABI are
+ * both live (builtin_cg_type_init already consults c->abi for the va_list
+ * builtin, so it is ready). */
+ for (u32 i = 0; i < KIT_CG_BUILTIN_COUNT; ++i) {
+ s->builtin_class[i] = api_compute_type_class(c, builtin_id((KitCgBuiltinType)i));
+ }
s->builtins_init = 1;
}
+u8 api_type_class(KitCg* g, KitCgTypeId ty) {
+ Compiler* c = g->c;
+ CgApiState* s;
+ CgApiType* e;
+ u32 seg;
+ if (ty == KIT_CG_TYPE_NONE) return 0;
+ s = c->cg_api ? (CgApiState*)c->cg_api : cg_api_get(c);
+ if (!s) return api_compute_type_class(c, ty);
+ seg = ty >> CG_API_TYPE_SEG_SHIFT;
+ if (seg == CG_API_TYPE_BUILTIN_SEG) {
+ u32 off = ty & CG_API_TYPE_SEG_MASK;
+ if (off < KIT_CG_BUILTIN_COUNT) return s->builtin_class[off];
+ return api_compute_type_class(c, ty);
+ }
+ e = api_type_from_id(c, ty);
+ if (!e) return api_compute_type_class(c, ty);
+ if (!(e->cached_class & API_TYPE_CLASS_CACHED))
+ e->cached_class = API_TYPE_CLASS_CACHED | api_compute_type_class(c, ty);
+ return (u8)(e->cached_class & ~API_TYPE_CLASS_CACHED);
+}
+
static CgApiState* cg_api_get(Compiler* c) {
Heap* h;
CgApiState* s;
@@ -174,8 +225,6 @@ static CgApiState* cg_api_get(Compiler* c) {
return s;
}
-static CgApiType* api_type_from_id(Compiler* c, KitCgTypeId id);
-
const CgType* cg_type_get(Compiler* c, KitCgTypeId id) {
u32 seg;
u32 off;
diff --git a/src/cg/value.c b/src/cg/value.c
@@ -68,11 +68,9 @@ int api_is_wide8_scalar_type(Compiler* c, KitCgTypeId ty) {
* float (e.g. double on a 32-bit ABI) bears int width 0 so it is not WK_WIDE8 —
* it falls through to WK_SOFT_DOUBLE, exactly as the original int-width guard in
* api_int_is_wide64 intended. */
-u8 api_wide_kind_for(KitCg* g, KitCgTypeId ty) {
- Compiler* c;
+u8 api_wide_kind_for(Compiler* c, KitCgTypeId ty) {
u8 fa;
- if (!g || !ty) return WK_NARROW;
- c = g->c;
+ if (!c || !ty) return WK_NARROW;
if (api_is_i128_type(c, ty)) return WK_I128;
if (api_is_f128_type(c, ty)) return WK_F128;
/* Int split into two 32-bit lanes by the ABI. The original api_int_is_wide64
@@ -97,57 +95,41 @@ u8 api_wide_kind_for(KitCg* g, KitCgTypeId ty) {
return WK_NARROW;
}
+/* The operand/value constructors run on the hottest per-operand codegen path.
+ * Each uses a designated compound literal rather than memset + field stores: it
+ * is value-identical (the unnamed fields zero-fill) but lets the compiler emit
+ * only the needed stores — and drop those it can see are immediately
+ * overwritten — instead of an out-of-line memset of the whole struct. Same
+ * idiom as the NativeLoc constructors in arch/native_target.h. */
Operand api_op_imm(i64 v, KitCgTypeId ty) {
- Operand o;
- memset(&o, 0, sizeof o);
- o.kind = OPK_IMM;
- o.type = ty;
- o.v.imm = v;
- return o;
+ return (Operand){.kind = OPK_IMM, .type = ty, .v.imm = v};
}
Operand api_op_local(CGLocal local, KitCgTypeId ty) {
- Operand o;
- memset(&o, 0, sizeof o);
- o.kind = OPK_LOCAL;
- o.type = ty;
- o.v.local = local;
- return o;
+ return (Operand){.kind = OPK_LOCAL, .type = ty, .v.local = local};
}
Operand api_op_global(ObjSymId sym, i64 addend, KitCgTypeId ty) {
- Operand o;
- memset(&o, 0, sizeof o);
- o.kind = OPK_GLOBAL;
- o.type = ty;
- o.v.global.sym = sym;
- o.v.global.addend = addend;
- return o;
+ return (Operand){.kind = OPK_GLOBAL,
+ .type = ty,
+ .v.global = {.sym = sym, .addend = addend}};
}
Operand api_op_indirect(CGLocal base, i32 ofs, KitCgTypeId ty) {
- Operand o;
- memset(&o, 0, sizeof o);
- o.kind = OPK_INDIRECT;
- o.type = ty;
- o.v.ind.base = base;
- o.v.ind.index = CG_LOCAL_NONE;
- o.v.ind.log2_scale = 0;
- o.v.ind.ofs = ofs;
- return o;
+ return (Operand){
+ .kind = OPK_INDIRECT,
+ .type = ty,
+ .v.ind = {.base = base, .index = CG_LOCAL_NONE, .ofs = ofs}};
}
Operand api_op_indirect_indexed(CGLocal base, CGLocal index, u8 log2_scale,
i32 ofs, KitCgTypeId ty) {
- Operand o;
- memset(&o, 0, sizeof o);
- o.kind = OPK_INDIRECT;
- o.type = ty;
- o.v.ind.base = base;
- o.v.ind.index = index;
- o.v.ind.log2_scale = log2_scale;
- o.v.ind.ofs = ofs;
- return o;
+ return (Operand){.kind = OPK_INDIRECT,
+ .type = ty,
+ .v.ind = {.base = base,
+ .index = index,
+ .log2_scale = log2_scale,
+ .ofs = ofs}};
}
u8 api_residency_for(const Operand* o) {
@@ -156,14 +138,15 @@ u8 api_residency_for(const Operand* o) {
}
ApiSValue api_make_sv(Operand op, KitCgTypeId ty) {
- ApiSValue sv;
- memset(&sv, 0, sizeof sv);
- sv.kind = SV_OPERAND;
- sv.op = op;
- sv.type = ty;
- sv.res = api_residency_for(&op);
- sv.source_local = KIT_CG_LOCAL_NONE;
- return sv;
+ /* Designated compound literal instead of memset: the unnamed fields (the
+ * 64-byte delayed union, the bitfield rider, the flags) still zero-fill, but
+ * the compiler emits direct stores for the live fields and skips the
+ * out-of-line memset of the whole node on this hot per-operand path. */
+ return (ApiSValue){.kind = SV_OPERAND,
+ .op = op,
+ .type = ty,
+ .res = api_residency_for(&op),
+ .source_local = KIT_CG_LOCAL_NONE};
}
ApiSValue api_make_lv(Operand op, KitCgTypeId ty) {
@@ -254,21 +237,25 @@ void api_stack_grow(KitCg* g, u32 want) {
void api_push(KitCg* g, ApiSValue v) {
KitCgTypeId ty = api_sv_type(&v);
+ /* One memoized fetch gives both the aggregate-place property and the
+ * wide/soft-float dispatch class — both pure functions of the type, computed
+ * once per type id rather than re-derived (cg_type_get + alias-chase + ABI
+ * round trips) on every push. */
+ u8 cls = api_type_class(g, ty);
/* An aggregate (record) can only ever be a PLACE: it is addressed, loaded,
* and passed by SRET/BYVAL/BYREF, never materialized as a scalar VALUE. Catch
* any aggregate VALUE at the point it would enter the stack. i128/f128 are
- * scalars (cg_type_is_aggregate is false for them), so they remain valid
- * VALUEs and are unaffected. */
- if (cg_type_is_aggregate(g->c, ty) && !api_is_lvalue_sv(&v)) {
+ * scalars (not aggregates), so they remain valid VALUEs and are unaffected. */
+ if ((cls & API_TYPE_CLASS_AGGREGATE) && !api_is_lvalue_sv(&v)) {
compiler_panic(g->c, g->cur_loc,
"KitCg: aggregate must be a place, not a value; load the "
"place or pass it by reference");
}
- /* Cache the wide/soft-float dispatch class once, here at the sole stack
- * writer; the wide and soft-float predicates in arith.c read it back instead
- * of re-deriving it per operand. dup/swap/rot copy the whole node (type and
- * tag together), so it stays valid for every later reader. */
- v.bitfield.wide_kind = api_wide_kind_for(g, ty);
+ /* Cache the wide/soft-float dispatch class on the node; the wide and
+ * soft-float predicates in arith.c read it back instead of re-deriving it per
+ * operand. dup/swap/rot copy the whole node (type and tag together), so it
+ * stays valid for every later reader. */
+ v.bitfield.wide_kind = cls & API_TYPE_CLASS_WIDE_MASK;
api_stack_grow(g, g->sp + 1);
g->stack[g->sp++] = v;
}