commit 486575cfb77911b5f89ce8f54eb8f3263ff32c96
parent fb17391cb69f024c19807c67bda922243b50d655
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Sat, 13 Jun 2026 10:09:20 -0700
perf(cg): -O0 carry lowered CgId on the parser value-stack slot (D.3b)
The parser's PcgSlot carried only const Type*, so pcg_mem / pcg_load re-derived
the KitCgTypeId via type_cg_id_in_pool (>1.2M calls) on every operand, feeding
the predicate/decode traffic api_type_pred sits on.
Carry the lowered cg_id ON the slot, stamped together with slot->type through a
single pcg_slot_set_type helper (every type-write site routed through it; whole-
slot dup/swap/rot3 copies carry it along). The id is lowered lazily on first
read (pcg_slot_cg_id) so an incomplete record stamped at push still lowers to
its real layout once completed — identical to a direct type_cg_id_in_pool call.
pcg_load reads the cached id via pcg_mem_id, skipping the bridge re-crossing
(and the double-lowering pcg_mem did for type + align).
Gate: full byte-identity gate PASS (the gate caught one missed type-restore
site mid-development — fixed); test-cg-api 211/0.
Diffstat:
2 files changed, 60 insertions(+), 15 deletions(-)
diff --git a/lang/c/parse/cg_adapter.c b/lang/c/parse/cg_adapter.c
@@ -6,12 +6,37 @@ KitCgTypeId pcg_tid(Parser* p, const Type* ty) {
return type_cg_id_in_pool(p->c, p->pool, ty);
}
-static u32 pcg_sizeof(Parser* p, const Type* ty) {
- return (u32)kit_cg_type_size(p->c, pcg_tid(p, ty));
+/* Set both a slot's C type and its lowered CG id together so the two never
+ * drift. Every site that writes slot->type must go through this (or copy a
+ * whole slot, which carries cg_id along). The id is lowered lazily on first
+ * read (pcg_slot_cg_id), not here, so an incomplete record stamped at push time
+ * still lowers to its real layout once completed -- identical to a direct
+ * type_cg_id_in_pool call. */
+static void pcg_slot_set_type(PcgSlot* s, const Type* ty) {
+ s->type = ty;
+ s->cg_id = KIT_CG_TYPE_NONE; /* lazily lowered on first read */
+}
+
+/* The slot's lowered CG id, lowering+caching on first read. A NULL type yields
+ * KIT_CG_TYPE_NONE every time (the sentinel also means "unfilled", but
+ * type_cg_id_in_pool(NULL) is constant so re-lowering is free). For a real type
+ * the first read crosses the bridge once; later reads are a load. */
+static KitCgTypeId pcg_slot_cg_id(Parser* p, PcgSlot* s) {
+ if (s->cg_id == KIT_CG_TYPE_NONE && s->type)
+ s->cg_id = type_cg_id_in_pool(p->c, p->pool, s->type);
+ return s->cg_id;
}
-static u32 pcg_alignof(Parser* p, const Type* ty) {
- return (u32)kit_cg_type_align(p->c, pcg_tid(p, ty));
+/* The cached CG id of the TOS slot's type, lowering lazily on first read.
+ * Empty-stack-safe (returns NONE), matching pcg_tid(p, pcg_top_type(p)). */
+static KitCgTypeId pcg_top_cg_id(Parser* p) {
+ return p->cg_type_sp
+ ? pcg_slot_cg_id(p, &p->cg_slot_stack[p->cg_type_sp - 1u])
+ : KIT_CG_TYPE_NONE;
+}
+
+static u32 pcg_sizeof(Parser* p, const Type* ty) {
+ return (u32)kit_cg_type_size(p->c, pcg_tid(p, ty));
}
#define PCG_VALUE_LVALUE 1u
@@ -29,15 +54,22 @@ static u8 pcg_lvalue_flags_for_type(const Type* ty) {
return flags;
}
-KitCgMemAccess pcg_mem(Parser* p, const Type* ty) {
+/* Build a MemAccess from an already-lowered CG id plus the C type (for its
+ * qualifiers). Lets a caller that already has the slot's cached cg_id skip the
+ * type_cg_id_in_pool re-crossing pcg_mem(ty) does twice (lower + align). */
+static KitCgMemAccess pcg_mem_id(Parser* p, KitCgTypeId id, const Type* ty) {
KitCgMemAccess m;
memset(&m, 0, sizeof m);
- m.type = pcg_tid(p, ty);
- m.align = pcg_alignof(p, ty);
+ m.type = id;
+ m.align = (u32)kit_cg_type_align(p->c, id);
if (ty && (ty->qual & Q_VOLATILE)) m.flags |= KIT_CG_MEM_VOLATILE;
return m;
}
+KitCgMemAccess pcg_mem(Parser* p, const Type* ty) {
+ return pcg_mem_id(p, pcg_tid(p, ty), ty);
+}
+
static void pcg_aux_clear(PcgLvAux* a) {
a->offset = 0;
a->scale = 0;
@@ -68,7 +100,7 @@ void pcg_push_type(Parser* p, const Type* ty) {
PcgSlot* s;
pcg_stack_grow(p, p->cg_type_sp + 1u);
s = &p->cg_slot_stack[p->cg_type_sp];
- s->type = ty;
+ pcg_slot_set_type(s, ty);
s->flags = 0;
pcg_aux_clear(&s->aux);
++p->cg_type_sp;
@@ -83,11 +115,12 @@ void pcg_dup_type(Parser* p) {
if (p->cg_type_sp) {
top = p->cg_slot_stack[p->cg_type_sp - 1u];
} else {
- top.type = NULL;
+ pcg_slot_set_type(&top, NULL);
top.flags = 0;
pcg_aux_clear(&top.aux);
}
- /* Copy the slot out before push: pcg_stack_grow may reallocate. */
+ /* Copy the slot out before push: pcg_stack_grow may reallocate. The whole
+ * slot (cg_id included) is copied back at the end, so dup preserves the id. */
pcg_push_type(p, top.type);
if (p->cg_type_sp) p->cg_slot_stack[p->cg_type_sp - 1u] = top;
}
@@ -157,7 +190,7 @@ const Type* pcg_top2_type(Parser* p) {
void pcg_retag_top(Parser* p, const Type* ty) {
if (p->cg_type_sp) {
- p->cg_slot_stack[p->cg_type_sp - 1u].type = ty;
+ pcg_slot_set_type(&p->cg_slot_stack[p->cg_type_sp - 1u], ty);
p->cg_slot_stack[p->cg_type_sp - 1u].flags = 0;
pcg_aux_clear(&p->cg_slot_stack[p->cg_type_sp - 1u].aux);
}
@@ -169,7 +202,7 @@ void pcg_retag_top(Parser* p, const Type* ty) {
* changes (e.g. struct unqualification, narrowing a compound-assign LHS). */
void pcg_retag_keep_flags(Parser* p, u32 depth, const Type* ty) {
if (p->cg_type_sp > depth)
- p->cg_slot_stack[p->cg_type_sp - 1u - depth].type = ty;
+ pcg_slot_set_type(&p->cg_slot_stack[p->cg_type_sp - 1u - depth], ty);
}
int pcg_top_is_bitfield(Parser* p) {
@@ -492,7 +525,7 @@ void pcg_load(Parser* p) {
int was_lvalue = pcg_top_is_lvalue(p);
if (pcg_emit_enabled(p)) {
PcgLvAux* lv = pcg_top_lv_aux(p);
- KitCgMemAccess access = pcg_mem(p, ty);
+ KitCgMemAccess access = pcg_mem_id(p, pcg_top_cg_id(p), ty);
/* Snapshot bit-field geometry before materialize clears the aux. */
PcgLvAux bf = lv ? *lv : (PcgLvAux){0};
/* Build the PLACE the strict load requires. A trivial local already has its
@@ -510,7 +543,10 @@ void pcg_load(Parser* p) {
kit_cg_load(p->cg, access);
}
if (was_lvalue && p->cg_type_sp) {
- p->cg_slot_stack[p->cg_type_sp - 1u].type = ty;
+ /* The emit block above can rewrite the TOS slot's type (materialize /
+ * deref push/drop), so restore it to the loaded value's type `ty` here --
+ * stamping cg_id alongside it. */
+ pcg_slot_set_type(&p->cg_slot_stack[p->cg_type_sp - 1u], ty);
p->cg_slot_stack[p->cg_type_sp - 1u].flags = 0;
pcg_aux_clear(&p->cg_slot_stack[p->cg_type_sp - 1u].aux);
}
diff --git a/lang/c/parse/cg_adapter.h b/lang/c/parse/cg_adapter.h
@@ -61,10 +61,19 @@ typedef struct PcgLvAux {
/* One shadow-stack slot: the CG type of a logical C value plus its parser-side
* value flags (PCG_VALUE_*) and lvalue aux, kept in lockstep with the CG value
- * stack. The parser pushes/dups/swaps/rotates whole slots. */
+ * stack. The parser pushes/dups/swaps/rotates whole slots.
+ *
+ * `cg_id` is the lowered KitCgTypeId of `type`, stamped together with `type`
+ * (see pcg_slot_set_type) so the hot pcg_mem / pcg_load queries read it directly
+ * instead of re-crossing the Type*->CgId bridge via type_cg_id_in_pool on every
+ * operand. KIT_CG_TYPE_NONE doubles as "type is NULL or not yet lowered"; the
+ * lazy filler (pcg_slot_cg_id) lowers on first read so it tracks any later
+ * record completion exactly like a direct type_cg_id_in_pool call would. cg_id
+ * is a pure function of `type`, so whole-slot copies (dup/swap/rot3) carry it. */
typedef struct PcgSlot {
const Type* type;
PcgLvAux aux;
+ KitCgTypeId cg_id;
u8 flags;
} PcgSlot;