kit

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

commit 442fdc5555d055f48a12b59956a60177ad9413a8
parent f75874cb64e948bc45a02ff2eb90bdb4b5457da3
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Sat, 13 Jun 2026 00:31:38 -0700

perf(cg): fuse scaled subscripts into one addressing mode at -O0

The C frontend lowered every scaled subscript (`a[i]`, `p->arr[i]`, `a[i].f`)
to an explicit `ptr_to_int; ×scale; +base; +ofs; int_to_ptr` pointer in
pcg_materialize_lv_to_ptr, then deref'd it — materializing a standalone pointer
temp (with an explicit multiply/add and a copy into the deref base, plus its
spill/reload) for what the backend can express as a single
`[base + index<<scale + #ofs]` addressing mode. The non-indexed member path
already folds its offset into the deref displacement; only the scaled path
bailed.

Route the scaled load/store place through the existing fused-EA primitive
instead. Add kit_cg_elem_scaled (a thin refactor of kit_cg_elem sharing
api_cg_elem) that scales the index by an EXPLICIT element size rather than the
base pointee size, so `a[i].f` can stride by sizeof(elem) while accessing the
field `f` (the load/store width comes from the explicit access type, not the
place). pcg_lv_to_memop_place now decays the base to a field_ty* pointer and
calls it for any in-int32-range scaled subscript; only out-of-range
displacements keep the explicit-pointer fallback.

Purely a frontend re-route onto a tested primitive — no backend change, no
liveness reasoning, landmine-immune (the fused place is consumed by exactly one
load/store; `s.a++ + ++s.b` stays 3). Faster to compile too (fewer CG ops/temps).

sqlite3.c -c, arm64-macOS: 586,773 -> 565,388 insns (-3.6%); object 2.74 -> 2.66MB;
reg-reg mov 63,383 -> 60,684. Running total this session: 831,078 -> 565,388
(-32%), ~1.65x tcc; compile-instructions 2.43B -> 2.18B.

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); struct-array-field,
2D, non-pow2-element, and store subscripts verified.

Diffstat:
Minclude/kit/cg.h | 9+++++++++
Mlang/c/parse/cg_adapter.c | 25+++++++++++++++++++++----
Msrc/cg/control.c | 13+++++++++++--
3 files changed, 41 insertions(+), 6 deletions(-)

diff --git a/include/kit/cg.h b/include/kit/cg.h @@ -777,6 +777,15 @@ KIT_API void kit_cg_field_bits(KitCg*, uint16_t bit_offset, uint16_t bit_width, */ KIT_API void kit_cg_elem(KitCg*, int64_t offset); +/* Like kit_cg_elem, but the index is scaled by an EXPLICIT `elem_size` instead + * of the base pointer's pointee size, and the resulting place's access type is + * the base pointee. Lets a frontend build `[base + index*elem_size + offset]` + * where the index stride (the containing array's element size) differs from the + * accessed sub-object's size — e.g. `arr[i].f` strides by sizeof(elem) while + * accessing the field `f`. The caller bitcasts the base to the access type + * first. Stack: [base, index] -> [place]. */ +KIT_API void kit_cg_elem_scaled(KitCg*, uint32_t elem_size, int64_t offset); + /* Load a VALUE from a PLACE / store a VALUE into a PLACE. The PLACE carries all * addressing (built via push_local / deref / field / elem); the memop takes no * effective-address rider. When the PLACE is a bit-field place (produced by diff --git a/lang/c/parse/cg_adapter.c b/lang/c/parse/cg_adapter.c @@ -602,14 +602,31 @@ static void pcg_lv_to_memop_place(Parser* p, const Type* field_ty) { i64 ofs = lv ? lv->offset : 0; u32 scale = lv ? lv->scale : 0u; const Type* fptr = type_ptr(p->pool, field_ty); - if (scale != 0u || ofs < INT32_MIN || ofs > INT32_MAX) { + if (ofs < INT32_MIN || ofs > INT32_MAX) { + /* Out-of-int32 displacement can't ride a deref/EA offset; materialize an + * explicit base+ofs pointer. */ pcg_materialize_lv_to_ptr(p, fptr); kit_cg_deref(p->cg, 0); return; } - if (base_kind == PCG_LV_BASE_LOCAL) kit_cg_addr(p->cg); - kit_cg_bitcast(p->cg, pcg_tid(p, fptr)); - kit_cg_deref(p->cg, ofs); + if (scale != 0u) { + /* Scaled subscript: build one fused [base + index*scale + ofs] place via + * kit_cg_elem_scaled rather than materializing a standalone + * base+index*scale+ofs pointer (an explicit mul/add + a copy into the deref + * base). CG stack on entry is [base, index]; decay the base to a field_ty* + * pointer value, then fuse. The index stride is the containing array's + * element size (scale) while the access type is field_ty, so `a[i].f` + * strides by sizeof(elem) but reads `f`. */ + kit_cg_swap(p->cg); /* [index, base] */ + if (base_kind == PCG_LV_BASE_LOCAL) kit_cg_addr(p->cg); /* [index, base_ptr] */ + kit_cg_bitcast(p->cg, pcg_tid(p, fptr)); /* [index, (T*)base] */ + kit_cg_swap(p->cg); /* [base, index] */ + kit_cg_elem_scaled(p->cg, scale, ofs); /* [place] */ + } else { + if (base_kind == PCG_LV_BASE_LOCAL) kit_cg_addr(p->cg); + kit_cg_bitcast(p->cg, pcg_tid(p, fptr)); + kit_cg_deref(p->cg, ofs); + } pcg_retag_top(p, fptr); { PcgLvAux* out = pcg_top_lv_aux(p); diff --git a/src/cg/control.c b/src/cg/control.c @@ -1050,7 +1050,10 @@ static int cg_scale_to_log2(u32 scale) { } } -void kit_cg_elem(KitCg* g, int64_t offset) { +/* Shared core for kit_cg_elem / kit_cg_elem_scaled. `elem_size` is the index + * stride in bytes; 0 means "derive from the base pointee size" (the kit_cg_elem + * behavior). The place's access type is always the base pointee. */ +static void api_cg_elem(KitCg* g, u32 elem_size, int64_t offset) { ApiSValue idx, base; CgTarget* T; KitCgTypeId base_ty, base_ptr_ty, elem_ty, idx_ty; @@ -1073,7 +1076,7 @@ void kit_cg_elem(KitCg* g, int64_t offset) { } elem_ty = base_info->ptr.pointee; base_ptr_ty = base_ty; - elemsz = (u32)abi_cg_sizeof(g->c->abi, elem_ty); + elemsz = elem_size ? elem_size : (u32)abi_cg_sizeof(g->c->abi, elem_ty); idx_ty = idx.type ? idx.type : idx.op.type; if (!idx_ty) idx_ty = builtin_id(KIT_CG_BUILTIN_I64); base_op = api_force_local(g, &base, base_ptr_ty); @@ -1127,6 +1130,12 @@ void kit_cg_elem(KitCg* g, int64_t offset) { } } +void kit_cg_elem(KitCg* g, int64_t offset) { api_cg_elem(g, 0u, offset); } + +void kit_cg_elem_scaled(KitCg* g, uint32_t elem_size, int64_t offset) { + api_cg_elem(g, elem_size, offset); +} + void kit_cg_field(KitCg* g, uint32_t field_index) { ApiSValue base; CgTarget* T;