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:
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;