kit

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

commit 5fe95f6d07bbf02f77cf5801af0aff3ee7f01e45
parent 1e0bc91f3e6f1089e6fa38b438a86b7c38b4b5ab
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Sat, 13 Jun 2026 12:41:52 -0700

perf(cg): aa64 -O0 positive-offset far-slot addressing (Lever 1 / Fix B)

A fixed frame slot past stur's signed-9-bit reach (-256) was addressed by
building its address first: sub xN,x29,#off (+ movk when off>4095) ; ldur [xN].
On sqlite that is 29,379 sub + ~4,000 movk just to reach locals.

Instead address such 4/8-byte int/fp slots at a positive *scaled* offset from a
stable bottom anchor: ldr/str [sp, #frame_size-off] reaches 32 KB in one insn,
zero address building. The frame size isn't known mid-body on the single-pass
path, so emit a one-word placeholder ldr/str [sp,#0] and an AA_PATCH_SLOT, then
rewrite the scaled offset in aa_func_end once the frame is final (reusing the
existing deferred-patch machinery). Top-record layout and CFI are unchanged
(fp still anchors the saved pair); only the slot access form changes, so out-
going-stack-arg frames are handled too.

Alloca floats sp, so such functions anchor a callee-saved AA_FRAME_BASE (x28) at
the frame base in the prologue (saved/restored like a callee-save, armed lazily
on the first alloca) and the slot patches address it instead of sp — same one-
word positive scaled form. Byte/half slots and Windows GP-home-area frames keep
the existing fp-relative path (their scaled reach is too small / the layout
shifts). Frames beyond dword scaled reach (32 KB at -O0) are a documented assert.

sqlite3.c -O0 .text: -25,579 insns (sub -22,273, movk -1,989); 1.3215x ->
1.2468x tcc. Gate: determinism + sqlite e2e O0/O1 (golden+vs-clang) + toy/parse/
smoke-x64/smoke-rv64/dwarf/debug all green; alloca (big frame + sp-move) and
far-slot/FP/addr-taken/stack-arg clang-differential probes match clang.

Diffstat:
Msrc/arch/aa64/native.c | 142++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 141 insertions(+), 1 deletion(-)

diff --git a/src/arch/aa64/native.c b/src/arch/aa64/native.c @@ -59,6 +59,12 @@ enum { aa_copy_bytes (which scratches only x16/x17) never clobbers */ AA_TMP0 = 16u, AA_TMP1 = 17u, + /* Callee-saved frame-base anchor. The single-pass far-slot fast path + * (aa_emit_mem / AA_PATCH_SLOT) addresses fixed slots at positive scaled + * offsets from a stable bottom anchor: sp itself when the function never moves + * sp, else this register, set to the frame base in the prologue and saved / + * restored like a callee-save (only when the function calls alloca). */ + AA_FRAME_BASE = 28u, AA_FP = 29u, AA_LR = 30u, AA_SP = 31u, @@ -190,14 +196,32 @@ typedef NativeFrameSlotEntry AANativeSlot; typedef enum AAPatchKind { AA_PATCH_ALLOCA, /* single instr: add dst, sp, #max_outgoing */ AA_PATCH_TAIL, /* AA_TAIL_WORDS region: callee restores + frame + br/b */ + AA_PATCH_SLOT, /* single instr: far fixed-slot ldr/str at a positive scaled + * offset from the frame base (sp, or AA_FRAME_BASE under + * alloca), resolved once frame_size is final. */ } AAPatchKind; +/* A deferred far fixed-slot access. The body emits a one-word placeholder + * `ldr/str [sp,#0]`; aa_apply_patches rewrites the base register and scaled + * offset once the frame is final. slot_off + extra is the slot's byte position + * (as fed to aa_fp_off_slot); the positive frame-base offset is + * aa_sp_off_saved_pair(L) + aa_fp_off_slot(slot)+extra (top-record). */ +typedef struct AASlotPatch { + u32 slot_off; /* aa_slot(...)->off */ + i32 extra; /* addr.offset added to the slot base */ + u8 sz; /* size_idx: 2 (word) or 3 (dword) */ + u8 vbit; /* SIMD/FP register */ + u8 load; /* 1 = ldr, 0 = str */ + u8 rt; /* transfer register */ +} AASlotPatch; + typedef struct AAPatch { AAPatchKind kind; u32 pos; union { u32 dst_reg; /* AA_PATCH_ALLOCA */ NativeLoc callee; /* AA_PATCH_TAIL */ + AASlotPatch slot; /* AA_PATCH_SLOT */ } u; } AAPatch; @@ -267,6 +291,21 @@ typedef struct AANativeTarget { * exclusive with slim_prologue (Tier A) and slim_small_frame; gated on * out_stack==0 && !has_alloca && frame_size <= 504. */ u8 fp_at_bottom; + + /* Single-pass far-slot fast path (Lever 1 / Fix B). When set, fixed-slot + * loads/stores whose top-record fp offset falls outside stur's ±256 range are + * emitted as a one-word positive scaled `ldr/str [base,#scaled]` placeholder + * plus an AA_PATCH_SLOT, instead of the multi-insn `sub xN,x29,#off ; ldur` + * address build. The base is a stable bottom anchor: sp (the common case) or + * AA_FRAME_BASE when the function moves sp via alloca. Decided at func_begin + * (off when a Windows GP home area shifts the layout); the frame-base fallback + * is armed lazily on the first alloca. Only 4/8-byte int/fp slots take this + * path — byte/half stay on the existing path (their positive scaled reach, + * 4 KB/8 KB, is too small to guarantee a one-word patch). */ + u8 slot_sp_base; + u8 uses_frame_base; /* an alloca fired: patch AA_PATCH_SLOT against + * AA_FRAME_BASE and set it up in the prologue. */ + NativeFrameSlot frame_base_slot; /* home for the saved caller AA_FRAME_BASE */ } AANativeTarget; static AANativeTarget* aa_of(NativeTarget* t) { return (AANativeTarget*)t; } @@ -763,6 +802,7 @@ static void aa_addr_base(AANativeTarget* a, NativeAddr addr, u32* base_out, static u32 aa_ldst_q_uimm(int load, u32 rt, u32 rn, u32 byte_off); static u32 aa_ldst_q_simm9(int load, u32 rt, u32 rn, i32 byte_off); +static AAPatch* aa_patch_alloc(AANativeTarget* a); /* far-slot deferral */ static void aa_emit_mem_q(AANativeTarget* a, int load, NativeLoc reg, NativeAddr addr) { @@ -802,6 +842,33 @@ static void aa_emit_mem(AANativeTarget* a, int load, NativeLoc reg, return; } if (native_loc_is_fp(reg) && sz < 2u) sz = 2u; + /* Far fixed-slot fast path (Lever 1 / Fix B). A plain 4/8-byte frame slot + * whose top-record fp offset is past stur's ±256 range would otherwise cost + * `sub xN,x29,#off (+movk) ; ldur` (2-3 insns). Instead emit a one-word + * positive scaled `ldr/str [sp,#0]` placeholder and defer the offset: once the + * frame is final, aa_apply_patches rewrites it to `[base, #frame_size-...]`, + * base = sp (stable) or AA_FRAME_BASE (alloca). Near slots keep their existing + * one-word ldur; byte/half stay on the fp path (scaled reach too small). */ + if (a->slot_sp_base && addr.base_kind == NATIVE_ADDR_BASE_FRAME && + addr.index_kind == NATIVE_ADDR_INDEX_NONE && (sz == 2u || sz == 3u)) { + AANativeSlot* s = aa_slot(a, addr.base.frame); + i32 fp_off = aa_fp_off_slot(a, s->off) + addr.offset; + if (fp_off < -256) { + u32 vbit = native_loc_is_fp(reg) ? 1u : 0u; + AAPatch* p = aa_patch_alloc(a); + p->kind = AA_PATCH_SLOT; + p->pos = mc_pos(mc); + p->u.slot.slot_off = s->off; + p->u.slot.extra = addr.offset; + p->u.slot.sz = (u8)sz; + p->u.slot.vbit = (u8)vbit; + p->u.slot.load = (u8)(load ? 1u : 0u); + p->u.slot.rt = (u8)rt; + aa_emit32(mc, load ? aa_ldr_uimm_v(sz, vbit, rt, AA_SP, 0) + : aa_str_uimm_v(sz, vbit, rt, AA_SP, 0)); + return; + } + } if (addr.base_kind == NATIVE_ADDR_BASE_GLOBAL && addr.index_kind == NATIVE_ADDR_INDEX_NONE) { i64 addend = addr.base.global.addend + (i64)addr.offset; @@ -1143,6 +1210,9 @@ static void aa_func_begin_common(NativeTarget* t, const CGFuncDesc* fd) { a->slim_small_frame = 0; a->fp_at_bottom = 0; a->frame_size_final = 0; + a->slot_sp_base = 0; + a->uses_frame_base = 0; + a->frame_base_slot = NATIVE_FRAME_SLOT_NONE; /* Windows variadic functions reserve a GP register home area at the top of * the frame (just below the incoming stack args). The plain-pointer va_list * then walks register-passed then stack-passed varargs as one block. Other @@ -1183,6 +1253,10 @@ static void aa_func_begin(NativeTarget* t, const CGFuncDesc* fd) { -(i32)((AA_FRAME_SAVE_SIZE + a->top_home_bytes) / 8u))); aa_emit32(mc, aa64_add_imm(1, AA_FP, AA_SP, 0, 0)); /* mov x29, sp */ + /* Arm the far-slot positive-scaled fast path for the common top-record + * layout. A Windows GP home area (top_home_bytes) places incoming args/home + * above the saved pair and is rare; leave those on the fp-relative path. */ + a->slot_sp_base = (a->top_home_bytes == 0u); /* Reserve only the deferred `sub sp` (+ Windows probe), patched in * aa_func_end. The region starts here; record it for the patch and CFI. */ region = abi_stack_probe_interval(a->base.c->abi) ? AA_NDT_SUB_WORDS @@ -1386,9 +1460,29 @@ static void aa_words_saved_pair_addr(AANativeTarget* a, u32* words, u32 cap, words[(*n)++] = aa64_add(1, AA_TMP1, AA_TMP1, AA_TMP0); } +/* Positive frame-base offset of the slot homing the caller's AA_FRAME_BASE + * (alloca functions only). Same positive-scaled form the far-slot patches use, + * so it is one word and fits any frame within dword scaled reach (32 KB). */ +static u32 aa_frame_base_save_off(AANativeTarget* a, const AAFrameLayout* L) { + i32 off = (i32)aa_sp_off_saved_pair(L) + + aa_fp_off_slot(a, aa_slot(a, a->frame_base_slot)->off); + if (off < 0 || ((u32)off >> 3) > 0xfffu) + aa_panic(a, "frame-base save slot out of positive scaled range"); + return (u32)off; +} + static void aa_words_restore_frame(AANativeTarget* a, u32* words, u32 cap, u32* n, const AAFrameLayout* L) { if (!L->frame_size) return; + /* Restore the caller's AA_FRAME_BASE before tearing the frame down. It still + * holds the frame base here (untouched by the body), so the slot is reachable + * as a positive scaled offset off itself; the load then overwrites it with + * the saved caller value. Alloca single-pass functions only. */ + if (a->uses_frame_base) { + u32 off = aa_frame_base_save_off(a, L); + if (*n >= cap) aa_panic(a, "epilogue too small for frame-base restore"); + words[(*n)++] = aa_ldr_uimm_v(3, 0, AA_FRAME_BASE, AA_FRAME_BASE, off); + } if (a->slim_prologue) { if (*n + 1u > cap) aa_panic(a, "instruction patch too small"); /* `ldp x29, x30, [sp], #16` — pop saved pair, restore sp. */ @@ -1565,6 +1659,17 @@ static u32 aa_build_ndt_sub_words(AANativeTarget* a, const AAFrameLayout* L, aa_words_stack_probe(a, words, cap, &n, sub_bytes, interval); } aa_words_sub_sp_frame(a, words, cap, &n, sub_bytes); + /* Set up the frame-base anchor for alloca functions: sp now points at the + * frame base, so save the caller's AA_FRAME_BASE into its slot (positive + * scaled, valid before the move) and anchor AA_FRAME_BASE = sp. The body + * never touches AA_FRAME_BASE (callee-saved; the single-pass path allocates + * caller-saved only), so it stays the frame base across any alloca. */ + if (a->uses_frame_base) { + u32 off = aa_frame_base_save_off(a, L); + if (n + 2u > cap) aa_panic(a, "prologue too small for frame-base setup"); + words[n++] = aa_str_uimm_v(3, 0, AA_FRAME_BASE, AA_SP, off); + words[n++] = aa64_add_imm(1, AA_FRAME_BASE, AA_SP, 0, 0); + } return n; } @@ -1636,6 +1741,23 @@ static void aa_apply_patches(AANativeTarget* a, const AAFrameLayout* L) { aa_panic(a, "outgoing area too large for alloca result"); aa_patch32(a->base.obj, sec, p->pos, aa64_add_imm(1, p->u.dst_reg, AA_SP, imm12, sh)); + } else if (p->kind == AA_PATCH_SLOT) { + /* Resolve a far fixed-slot access to a positive scaled load/store off the + * stable bottom anchor. The slot sits at fp_off below fp (top-record); + * adding the bytes the prologue subtracted to reach the frame base + * (aa_sp_off_saved_pair) gives its offset from sp / AA_FRAME_BASE. */ + const AASlotPatch* sl = &p->u.slot; + i32 fp_off = aa_fp_off_slot(a, sl->slot_off) + sl->extra; + i32 base_off = (i32)aa_sp_off_saved_pair(L) + fp_off; + u32 base = a->uses_frame_base ? AA_FRAME_BASE : AA_SP; + if (base_off < 0 || ((u32)base_off & ((1u << sl->sz) - 1u)) != 0u || + ((u32)base_off >> sl->sz) > 0xfffu) + aa_panic(a, "far slot offset out of positive scaled range"); + aa_patch32(a->base.obj, sec, p->pos, + sl->load ? aa_ldr_uimm_v(sl->sz, sl->vbit, sl->rt, base, + (u32)base_off) + : aa_str_uimm_v(sl->sz, sl->vbit, sl->rt, base, + (u32)base_off)); } else { /* AA_PATCH_TAIL */ NativeLoc callee = p->u.callee; u32 words[AA_TAIL_WORDS]; @@ -2628,7 +2750,25 @@ static void aa_alloca(NativeTarget* t, NativeLoc dst, NativeLoc size, aa_panic(a, "outgoing area too large for alloca result"); aa_emit32(t->mc, aa64_add_imm(1, loc_reg(dst), AA_SP, imm12, sh)); } else { - AAPatch* p = aa_patch_alloc(a); + AAPatch* p; + /* First alloca arms the frame-base anchor. The far-slot fast path can no + * longer key off sp (this `sub sp` floats it), so deferred AA_PATCH_SLOTs — + * those already emitted and any still to come — resolve against + * AA_FRAME_BASE instead. Reserve a static-frame home for the caller's value; + * the prologue saves it and sets AA_FRAME_BASE = frame base (before the body + * runs, so every slot access sees a valid anchor), the epilogue restores + * it. Only needed when slot_sp_base armed the fast path in the first place. */ + if (a->slot_sp_base && !a->uses_frame_base) { + NativeFrameSlotDesc sd; + memset(&sd, 0, sizeof sd); + sd.type = builtin_id(KIT_CG_BUILTIN_I64); + sd.size = 8; + sd.align = 8; + sd.kind = NATIVE_FRAME_SLOT_SAVE; + a->frame_base_slot = t->frame_slot(t, &sd); + a->uses_frame_base = 1; + } + p = aa_patch_alloc(a); p->kind = AA_PATCH_ALLOCA; p->pos = mc_pos(t->mc); p->u.dst_reg = loc_reg(dst);