kit

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

commit 9a99a0aae81d593f98884d5b2a8a2b94b3e1aecf
parent a69929714a36eaaad30511e1582b14c81e63cab1
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Mon, 15 Jun 2026 23:43:10 -0700

aa64: uniform x29-at-bottom known-frame layout at O1 (O1.md W1.1)

Diffstat:
Mmk/test.mk | 8+++++++-
Msrc/arch/aa64/native.c | 347++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
Mtest/opt/aa64_tail_call.sh | 29++++++++++++++++++++++++++---
Atest/opt/aa64_x29_bottom.sh | 152+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest/opt/redundant_frame_sub.sh | 24+++++++++++++-----------
5 files changed, 427 insertions(+), 133 deletions(-)

diff --git a/mk/test.mk b/mk/test.mk @@ -859,7 +859,7 @@ test-macho: lib $(TEST_RT_DEP) $(ROUNDTRIP_BIN_MACHO) $(LINK_EXE_RUNNER) $(JIT_R OPT_TEST_BIN = build/test/cg_ir_lower_test TINY_INLINE_TEST_BIN = build/test/tiny_inline_test -test-opt: bin $(OPT_TEST_BIN) test-opt-tiny-inline test-opt-inline test-opt-zero-arg test-opt-static-prune-aa64 test-opt-aa64-tail test-opt-x64-win-tail-sret test-opt-prologue-tier test-opt-whole-program-inline test-opt-lto-phase1 test-opt-redundant-copy-ext test-opt-redundant-frame-sub test-opt-o1-branch-cleanup test-opt-hot-slot-order test-opt-o1-remat +test-opt: bin $(OPT_TEST_BIN) test-opt-tiny-inline test-opt-inline test-opt-zero-arg test-opt-static-prune-aa64 test-opt-aa64-tail test-opt-x64-win-tail-sret test-opt-prologue-tier test-opt-whole-program-inline test-opt-lto-phase1 test-opt-redundant-copy-ext test-opt-redundant-frame-sub test-opt-o1-branch-cleanup test-opt-hot-slot-order test-opt-o1-remat test-opt-aa64-x29-bottom $(OPT_TEST_BIN) @@ -893,6 +893,12 @@ test-opt-hot-slot-order: bin test-opt-o1-remat: bin @KIT=$(abspath $(BIN)) bash test/opt/o1_remat.sh +# Structural disasm check: aa64 x29-at-bottom known-frame layout addresses every +# spill slot as positive ldr/str [x29,#k] (or add-build) -- no sub xN,x29,# at O1. +.PHONY: test-opt-aa64-x29-bottom +test-opt-aa64-x29-bottom: bin + @KIT=$(abspath $(BIN)) bash test/opt/aa64_x29_bottom.sh + test-opt-tiny-inline: bin $(TINY_INLINE_TEST_BIN) $(TINY_INLINE_TEST_BIN) diff --git a/src/arch/aa64/native.c b/src/arch/aa64/native.c @@ -74,8 +74,13 @@ enum { * place; the optimizer path reserves exactly what it emits). Sized to hold * the fat prologue plus the Windows large-frame stack probe (≤7 words, see * aa_words_stack_probe). */ - AA_PROLOGUE_WORDS = 32u, - AA_TAIL_WORDS = 32u, + /* Worst-case prologue/epilogue word counts. The W1.1 bottom-record general + * path can stack: a Windows page probe (≤7), the robust `sub sp` (≤5), the + * far saved-pair address build + stp (≤3), the x29 anchor (≤3), a callee-save + * base build for huge frames (≤4), and up to 18 callee-save store/restores + * (~5 int stp pairs + an int single + 8 fp singles ≈ 14 insns) — round up. */ + AA_PROLOGUE_WORDS = 48u, + AA_TAIL_WORDS = 48u, /* Single-pass (-O0) tcc-style prologue. The frame-independent entry * `stp x29,x30,[sp,#-(16+top_home)]! ; mov x29,sp` (AA_NDT_FIXED_ENTRY_WORDS) * is emitted live in aa_func_begin; only the `sub sp` that grows the rest of @@ -125,27 +130,40 @@ enum { * sp --> +------------------------------+ * low addr CFA = fp + 16 * - * BOTTOM-RECORD (fp_at_bottom — known-frame -O1 small frames with - * callee-saves/locals and out_stack==0). The record moves to the bottom so the - * sp adjustment folds into a pre/post-indexed stp/ldp (−2 insns/call). fp = sp; - * slots/callee-saves stack ABOVE the record at positive offsets. Offsets depend - * on frame_size (hence known-frame only, where the frame is final before body). + * BOTTOM-RECORD (fp_at_bottom — the W1.1 uniform x29-at-bottom layout: every + * known frame on the -O1 path EXCEPT slim Tier A and the Windows-variadic + * top_home case). The frame record + slots stack ABOVE x29, anchored just above + * the outgoing-arg area: x29 = sp + out_stack. Every slot is then a positive + * `ldr/str [x29,#k]` — one instruction within the 32 KB scaled reach (add-build + * `add x16,x29,#hi ; ldr [x16,#lo]` only past it), replacing the top-record + * `sub x17,x29,#k ; ldur` fallback. x29 is the frame pointer (reserved, + * alloca-stable, set once) so alloca falls out for free and outgoing args stay + * sp-relative (calls after an alloca still address [sp,#k]). Offsets depend on + * frame_size (hence known-frame only, where the frame is final before the body). + * Let N = frame_size, os = out_stack: * * high addr caller's stack frame * +------------------------------+ - * | incoming stack args | aa_fp_off_in_arg(a,i) = N+i - * +------------------------------+ <- caller's sp = CFA = fp + - * N | frame slots (+ align pad) | aa_fp_off_slot(a,off)=N-off | - * (callee-saves + locals …) | (in [16, N), above record) + * | incoming stack args | aa_fp_off_in_arg(a,i)=(N-os)+i + * +------------------------------+ <- caller's sp = CFA = x29+(N-os) + * | frame slots (+ align pad) | aa_fp_off_slot(a,off)=(N-os)-off + * | (callee-saves + locals …) | (in [16, N-os), above record) * +------------------------------+ - * fp = sp --> | saved x29 (prev fp) | aa_fp_off_saved_fp() = 0 + * x29 = sp+os->| saved x29 (prev fp) | aa_fp_off_saved_fp() = 0 * | saved x30 (prev lr) | aa_fp_off_saved_lr() = 8 - * low addr +------------------------------+ (N = frame_size; - * out_stack==0) + * +------------------------------+ + * | outgoing args (os) | aa_sp_off_out_arg(i) ([sp,#i]) + * sp --> +------------------------------+ + * low addr * * frame_size (N) = align16(AA_FRAME_SAVE_SIZE + slot_bytes + out_stack). - * Tail calls write outgoing args into the caller's incoming-args window — - * physically the same address, expressed via aa_fp_off_tail_out_arg. + * The saved pair sits AT x29 so [x29]=caller fp, [x29+8]=caller lr — the + * frame-pointer chain kit's unwinder / __kit_backtrace walks (uniform + * fp[0]/fp[1]); non-negotiable. When os==0 this reduces to fp = sp and the + * earlier folded `stp [sp,#-N]! ; mov x29,sp` encoding (the original + * fp_at_bottom fast path). Tail calls write outgoing args into the caller's + * incoming-args window — physically the same address, expressed via + * aa_fp_off_tail_out_arg. * ========================================================================== */ typedef struct AAFrameLayout { @@ -243,6 +261,13 @@ typedef struct AANativeTarget { * layout (where slot/incoming-arg offsets depend on frame_size); meaningless * and unread on the single-pass path, which never sets fp_at_bottom. */ u32 frame_size_final; + /* Final outgoing-arg area (= max_outgoing), set with frame_size_final in + * aa_func_begin_known_frame. In the bottom-record layout x29 is anchored + * `out_stack` bytes above sp, so every fp-relative offset is measured from + * `frame_size - out_stack` (the bytes above the anchor). Zero on the + * single-pass path (top-record) and on os==0 known frames (where the formula + * reduces to the original fp_at_bottom one). */ + u32 out_stack_final; u32 incoming_stack_size; /* Windows-variadic GP register home area size (gp_reg_count * gp_slot_size, * 64 today; 0 on every other ABI). When nonzero the function takes the fat @@ -274,25 +299,18 @@ typedef struct AANativeTarget { * emit a 2-insn `stp x29,x30,[sp,#-16]! ; mov x29,sp` and matching `ldp * x29,x30,[sp],#16 ; ret` instead of the fat 4+3-insn FP-frame form. */ u8 slim_prologue; - /* Set at func_end when frame_size - 16 fits stp's signed 7-bit scaled - * immediate (frame_size <= 520). Skips the `add x17, sp, #(N-16)` scratch - * materialization in the prologue (stp x29,x30,[sp,#N-16] instead) and - * the matching `add x10, fp, #0` in the epilogue (ldp x29,x30,[sp,#N-16] - * + add sp,sp,#N). Mutually exclusive with `slim_prologue` (Tier A wins - * when both would apply) and `fp_at_bottom` (which wins for out_stack==0). - * Now only reached for small frames with outgoing stack args (out_stack>0), - * where the record cannot move to the bottom. Keeps the top-record layout. */ - u8 slim_small_frame; - /* Set by aa_func_begin_known_frame for a small frame with callee-saves/locals - * and no outgoing stack args: the frame record moves to the bottom of the - * frame (fp = sp, `mov x29,sp`) so the sp adjustment folds into a pre-indexed - * `stp x29,x30,[sp,#-N]!` entry and post-indexed `ldp x29,x30,[sp],#N` exit - * (−2 insns/call vs slim_small_frame). Slots and callee-saves stack ABOVE the - * record at positive fp offsets; incoming args sit at fp+frame_size; CFA = - * fp+frame_size. The frame-size-dependent offsets are the reason this is only - * available on the known-frame path (frame final before the body). Mutually - * exclusive with slim_prologue (Tier A) and slim_small_frame; gated on - * out_stack==0 && !has_alloca && frame_size <= 504. */ + /* Set by aa_func_begin_known_frame for the W1.1 uniform x29-at-bottom layout: + * every known frame except slim Tier A and the Windows-variadic top_home + * case. x29 is anchored out_stack bytes above sp with the saved pair AT x29; + * every slot is a positive `ldr/str [x29,#k]`, slots/callee-saves stack ABOVE + * the record, incoming args at x29 + (frame_size - out_stack) = CFA. Covers + * out_stack>0, alloca (x29 is stable across the floated sp), and arbitrarily + * large frames. Two prologue/epilogue encodings: a folded `stp [sp,#-N]!` + * fast path when out_stack==0 && frame_size<=504 && !alloca, and the general + * `sub sp ; stp [sp,#os] ; add x29,sp,#os` / `mov x16,x29 ; ldp [x16] ; + * add sp,x16,#(N-os)` form otherwise. The frame-size-dependent offsets are + * the reason this is only available on the known-frame path (frame final + * before the body). Mutually exclusive with slim_prologue (Tier A). */ u8 fp_at_bottom; /* Single-pass far-slot fast path (Lever 1 / Fix B). When set, fixed-slot @@ -318,20 +336,24 @@ static AANativeTarget* aa_of(NativeTarget* t) { return (AANativeTarget*)t; } * * top-record (default): record near the top, fp anchored at the saved pair. * incoming args at fp+16+b, slots below fp at -off. CFA = fp+16. - * bottom-record (fp_at_bottom): record at the bottom, fp = sp. - * incoming args at fp+frame_size+b, slots above the record at - * frame_size-off (in [16, frame_size), never overlapping the 16-byte - * record since frame_size = align16(16+cum_off) >= 16+cum_off). - * CFA = fp+frame_size. */ + * bottom-record (fp_at_bottom): x29 = sp + out_stack, the saved pair AT x29, + * slots above the record at (frame_size - out_stack) - off, incoming args + * at x29 + (frame_size - out_stack). CFA = x29 + (frame_size - out_stack). + * The "bytes above the anchor" is bsz = frame_size - out_stack = align16(16 + * + slot_bytes) padding aside, so slots land in [16, bsz) and never overlap + * the 16-byte record. When out_stack==0, bsz = frame_size (fp = sp). */ +static inline u32 aa_fp_bottom_above(const AANativeTarget* a) { + return a->frame_size_final - a->out_stack_final; +} static inline i32 aa_fp_off_in_arg(const AANativeTarget* a, u32 byte_off) { /* top-record incoming args sit above the saved pair and the (usually empty) * Windows-variadic GP home area; bottom-record never carries a home area. */ - u32 base = a->fp_at_bottom ? a->frame_size_final + u32 base = a->fp_at_bottom ? aa_fp_bottom_above(a) : AA_FRAME_SAVE_SIZE + a->top_home_bytes; return (i32)(base + byte_off); } static inline i32 aa_fp_off_slot(const AANativeTarget* a, u32 slot_off) { - return a->fp_at_bottom ? (i32)a->frame_size_final - (i32)slot_off + return a->fp_at_bottom ? (i32)aa_fp_bottom_above(a) - (i32)slot_off : -(i32)slot_off; } /* Outgoing stack args on a tail call land in the caller's incoming-arg window — @@ -342,10 +364,10 @@ static inline i32 aa_fp_off_tail_out_arg(const AANativeTarget* a, return aa_fp_off_in_arg(a, byte_off); } /* CFA = caller's sp, expressed as an fp-relative offset (fp+16 top-record, - * fp+frame_size bottom-record). Named so the CFI emit site stays layout-blind. - */ + * x29 + (frame_size - out_stack) bottom-record). Named so the CFI emit site + * stays layout-blind. */ static inline i32 aa_cfa_off(const AANativeTarget* a) { - return a->fp_at_bottom ? (i32)a->frame_size_final + return a->fp_at_bottom ? (i32)aa_fp_bottom_above(a) : (i32)(AA_FRAME_SAVE_SIZE + a->top_home_bytes); } @@ -1299,9 +1321,9 @@ static void aa_func_begin_common(NativeTarget* t, const CGFuncDesc* fd) { a->npatches = 0; a->nalloca = 0; a->slim_prologue = 0; - a->slim_small_frame = 0; a->fp_at_bottom = 0; a->frame_size_final = 0; + a->out_stack_final = 0; a->slot_sp_base = 0; a->uses_frame_base = 0; a->frame_base_slot = NATIVE_FRAME_SLOT_NONE; @@ -1582,27 +1604,43 @@ static void aa_words_restore_frame(AANativeTarget* a, u32* words, u32 cap, return; } if (a->fp_at_bottom) { - /* Bottom-record fold: `ldp x29,x30,[sp],#N` reloads the pair from the - * bottom AND releases the whole frame in one insn. Callee-saves were - * already restored by aa_emit_callee_restores. -1 insn vs slim_small_frame - * (which needs a separate `add sp`). N <= 504 holds the post-index imm. */ - if (*n + 1u > cap) aa_panic(a, "instruction patch too small"); - words[(*n)++] = - aa64_ldp64_post(AA_FP, AA_LR, AA_SP, (i32)(L->frame_size / 8u)); - return; - } - if (a->slim_small_frame) { - /* `ldp x29,x30,[sp,#saved_pair] ; add sp,sp,#frame_size` — load through - * sp avoids the fat path's `add x10, fp, #0` scratch, and the subsequent - * `add sp` unwinds without depending on the (now-clobbered) old fp. */ - u32 save_off = aa_sp_off_saved_pair(L); - u32 imm12, sh; - if (*n + 2u > cap) aa_panic(a, "instruction patch too small"); - words[(*n)++] = aa64_ldp64_soff(AA_FP, AA_LR, AA_SP, (i32)(save_off / 8u)); - if (!aa64_addsub_imm_fits(L->frame_size, &imm12, &sh)) - aa_panic(a, "slim_small_frame: frame_size out of addsub imm range"); - words[(*n)++] = aa64_add_imm(1, AA_SP, AA_SP, imm12, sh); - return; + u32 os = L->out_stack; + if (os == 0u && !a->frame.has_alloca && L->frame_size <= 504u) { + /* Folded fast path (matches the os==0 prologue fold): the saved pair sits + * at the very bottom (= sp, since no alloca floated it), so + * `ldp x29,x30,[sp],#N` reloads the pair AND releases the whole frame in + * one insn. Callee-saves were already restored by aa_emit_callee_restores. + * N <= 504 holds the post-index imm. */ + if (*n + 1u > cap) aa_panic(a, "instruction patch too small"); + words[(*n)++] = + aa64_ldp64_post(AA_FP, AA_LR, AA_SP, (i32)(L->frame_size / 8u)); + return; + } + /* General bottom-record teardown, correct with OR without alloca (the + * post-indexed `ldp [sp],#N` can't be used once alloca floated sp): recover + * sp from the stable anchor x29, not from sp. `mov x16,x29 ; ldp x29,x30, + * [x16] ; add sp,x16,#(fs-os)` — x16 holds the anchor across the pair reload + * so the final add restores sp = x29 + (fs-os) = caller's sp = CFA. */ + { + u32 bsz = L->frame_size - os; /* = aa_cfa_off in this layout */ + u32 imm12, sh; + if (*n + 2u > cap) aa_panic(a, "instruction patch too small"); + words[(*n)++] = aa64_add_imm(1, AA_TMP0, AA_FP, 0, 0); /* mov x16, x29 */ + words[(*n)++] = aa64_ldp64_soff(AA_FP, AA_LR, AA_TMP0, 0); + if (aa64_addsub_imm_fits(bsz, &imm12, &sh)) { + if (*n >= cap) aa_panic(a, "instruction patch too small"); + words[(*n)++] = aa64_add_imm(1, AA_SP, AA_TMP0, imm12, sh); + } else { + /* bsz exceeds the addsub-imm window. The 3-register add cannot target + * SP (Rd=31 means XZR there), so sum into x16 (a normal register) then + * copy to sp via the immediate `add sp, x16, #0` form (Rd=31 == SP). */ + aa_words_load_imm(a, words, cap, n, AA_TMP1, bsz); + if (*n + 2u > cap) aa_panic(a, "instruction patch too small"); + words[(*n)++] = aa64_add(1, AA_TMP0, AA_TMP0, AA_TMP1); + words[(*n)++] = aa64_add_imm(1, AA_SP, AA_TMP0, 0, 0); + } + return; + } } if (aa_cfa_off(a) == AA_FRAME_SAVE_SIZE) { /* Common top-record case (CFA == 16: no Windows-variadic GP home area). The @@ -1631,16 +1669,48 @@ static void aa_words_restore_frame(AANativeTarget* a, u32* words, u32 cap, * reserve_callee_saves allocates consecutive 8-byte slots in order, so * callee_saves[i] sits 8 bytes above callee_saves[i+1]; for an int pair the * lower-addressed reg[i+1] is the stp's Rt and reg[i] is Rt2. FP registers (and - * an unpaired trailing int) use the single-register stur/ldur form. */ + * an unpaired trailing int) use the single-register stur/ldur form. + * + * Layout reach. Top-record callee-saves sit nearest fp at small NEGATIVE offsets + * (reserved first), so they always fit stur/stp directly. Bottom-record (W1.1) + * anchors x29 at the bottom, so callee-saves stack near the TOP at large + * POSITIVE offsets — past stp's +504 reach in any frame above ~512 bytes (and + * past the scaled str reach of 32 KB in a huge frame). When the highest + * callee-save offset exceeds stp's reach we materialize a base register + * `x16 = x29 + adj` once (adj 16-aligned so it stays addsub-encodable) and + * address the whole callee-save block relative to it at small offsets — x16 is + * dead here on both the prologue (after the x29 anchor) and epilogue (callee + * restores precede the teardown that reuses x16) paths. */ static void aa_words_callee_saves(AANativeTarget* a, int save, u32* words, u32 cap, u32* n) { + u32 base = AA_FP; + i32 adj = 0; + if (a->fp_at_bottom && a->frame.ncallee_saves) { + /* The first reserved callee-save (smallest slot.off) gets the largest fp + * offset; that bound decides whether a base register is needed. */ + i32 hi = aa_fp_off_slot(a, aa_slot(a, a->frame.callee_saves[0].slot)->off); + if (hi > 504) { + u32 imm12, sh; + adj = (i32)((u32)(hi - 504) & ~15u); /* 16-aligned, leaves hi-adj <= 504 */ + if (!aa64_addsub_imm_fits((u32)adj, &imm12, &sh)) { + /* adj is a multiple of 4096 worth of frame; build it via x16 load_imm. */ + aa_words_load_imm(a, words, cap, n, AA_TMP0, adj); + if (*n + 1u > cap) aa_panic(a, "prologue too large"); + words[(*n)++] = aa64_add(1, AA_TMP0, AA_FP, AA_TMP0); + } else { + if (*n >= cap) aa_panic(a, "prologue too large"); + words[(*n)++] = aa64_add_imm(1, AA_TMP0, AA_FP, imm12, sh); + } + base = AA_TMP0; + } + } for (u32 i = 0; i < a->frame.ncallee_saves;) { const AACalleeSave* cs = &a->frame.callee_saves[i]; - i32 off = aa_fp_off_slot(a, aa_slot(a, cs->slot)->off); + i32 off = aa_fp_off_slot(a, aa_slot(a, cs->slot)->off) - adj; if (i + 1u < a->frame.ncallee_saves && cs->cls == (u8)NATIVE_REG_INT && a->frame.callee_saves[i + 1u].cls == (u8)NATIVE_REG_INT) { const AACalleeSave* cs2 = &a->frame.callee_saves[i + 1u]; - i32 off2 = aa_fp_off_slot(a, aa_slot(a, cs2->slot)->off); + i32 off2 = aa_fp_off_slot(a, aa_slot(a, cs2->slot)->off) - adj; /* cs2 is reserved after cs (larger slot.off), so it is the lower address * in both layouts (off2 = off - 8): stp's Rt = cs2, Rt2 = cs, base off2. * stp/ldp's signed-7-bit scaled immediate reaches ±504. */ @@ -1648,25 +1718,25 @@ static void aa_words_callee_saves(AANativeTarget* a, int save, u32* words, aa_panic(a, "callee-save pair offset out of prologue range"); if (*n >= cap) aa_panic(a, "prologue too large"); words[(*n)++] = save - ? aa64_stp64_soff(cs2->reg, cs->reg, AA_FP, off2 / 8) - : aa64_ldp64_soff(cs2->reg, cs->reg, AA_FP, off2 / 8); + ? aa64_stp64_soff(cs2->reg, cs->reg, base, off2 / 8) + : aa64_ldp64_soff(cs2->reg, cs->reg, base, off2 / 8); i += 2u; } else { u32 v = cs->cls == (u8)NATIVE_REG_FP ? 1u : 0u; if (*n >= cap) aa_panic(a, "prologue too large"); if (a->fp_at_bottom) { - /* Positive, 8-aligned offset above the record (up to frame_size-8 ≤ - * 496): the unscaled stur (±256) can't reach it, so use the scaled - * unsigned-imm str/ldr. */ + /* Positive, 8-aligned offset (after the optional base adjustment): the + * unscaled stur (±256) can't reach it, so use the scaled unsigned-imm + * str/ldr (reach 32 KB; adj keeps offsets in range for huge frames). */ if (off < 0 || (u32)off > 0x7ff8u) aa_panic(a, "callee-save offset out of prologue range"); - words[(*n)++] = save ? aa_str_uimm_v(3, v, cs->reg, AA_FP, (u32)off) - : aa_ldr_uimm_v(3, v, cs->reg, AA_FP, (u32)off); + words[(*n)++] = save ? aa_str_uimm_v(3, v, cs->reg, base, (u32)off) + : aa_ldr_uimm_v(3, v, cs->reg, base, (u32)off); } else { if (off < -256 || off > 255) aa_panic(a, "callee-save offset out of prologue range"); - words[(*n)++] = save ? aa_stur_v(3, v, cs->reg, AA_FP, off) - : aa_ldur_v(3, v, cs->reg, AA_FP, off); + words[(*n)++] = save ? aa_stur_v(3, v, cs->reg, base, off) + : aa_ldur_v(3, v, cs->reg, base, off); } i += 1u; } @@ -1696,14 +1766,64 @@ static u32 aa_build_prologue_words(AANativeTarget* a, const AAFrameLayout* L, return n; } if (a->fp_at_bottom) { - /* Bottom-record fold: `stp x29,x30,[sp,#-N]!` decrements sp by the whole - * frame AND saves the pair at the new bottom in one insn; `mov x29,sp` - * (add #0) anchors fp there. Callee-saves then stack above the record at - * positive offsets. -2 insns/call vs the top-record slim_small_frame. */ - if (n + 2u > cap) aa_panic(a, "prologue too large"); - words[n++] = - aa64_stp64_pre(AA_FP, AA_LR, AA_SP, -(i32)(L->frame_size / 8u)); - words[n++] = aa64_add_imm(1, AA_FP, AA_SP, 0, 0); + /* W1.1 uniform x29-at-bottom: anchor x29 = sp + out_stack, saved pair AT + * x29, slots above it. Two encodings: */ + u32 os = L->out_stack; + u32 imm12, sh; + if (os == 0u && L->frame_size <= 504u) { + /* Folded fast path (the original fp_at_bottom): the saved pair is at the + * very bottom, so `stp x29,x30,[sp,#-N]!` decrements sp by the whole frame + * AND saves the pair in one insn; `mov x29,sp` anchors fp there. The + * post-index ldp on exit needs N <= 504. */ + if (n + 2u > cap) aa_panic(a, "prologue too large"); + words[n++] = + aa64_stp64_pre(AA_FP, AA_LR, AA_SP, -(i32)(L->frame_size / 8u)); + words[n++] = aa64_add_imm(1, AA_FP, AA_SP, 0, 0); + aa_words_callee_saves(a, 1, words, cap, &n); + return n; + } + /* General bottom-record. `sub sp,sp,#fs` (robust / Windows-probed); save the + * pair at [sp,#os] and anchor x29 = sp + os; callee-saves at positive x29 + * offsets. Outgoing args occupy [sp, sp+os); the body addresses them + * sp-relative, so anchoring x29 above them keeps every slot positive while + * leaving the arg area reachable from the (possibly alloca-floated) sp. */ + { + u32 interval = abi_stack_probe_interval(a->base.c->abi); + if (interval && L->frame_size > interval) + aa_words_stack_probe(a, words, cap, &n, L->frame_size, interval); + } + aa_words_sub_sp_frame(a, words, cap, &n, L->frame_size); + if (os <= 504u) { + /* `stp x29,x30,[sp,#os]` — os fits stp's signed-7-bit scaled imm. */ + if (n >= cap) aa_panic(a, "prologue too large"); + words[n++] = aa64_stp64_soff(AA_FP, AA_LR, AA_SP, (i32)(os / 8u)); + } else { + /* Far outgoing area: build the saved-pair address (sp + os) into x17. The + * 3-register add cannot read SP (Rn=31 means XZR), so copy sp into x17 via + * the immediate `add x17, sp, #0` form first, then add os. */ + if (aa64_addsub_imm_fits(os, &imm12, &sh)) { + if (n >= cap) aa_panic(a, "prologue too large"); + words[n++] = aa64_add_imm(1, AA_TMP1, AA_SP, imm12, sh); + } else { + aa_words_load_imm(a, words, cap, &n, AA_TMP0, os); + if (n + 2u > cap) aa_panic(a, "prologue too large"); + words[n++] = aa64_add_imm(1, AA_TMP1, AA_SP, 0, 0); + words[n++] = aa64_add(1, AA_TMP1, AA_TMP1, AA_TMP0); + } + if (n >= cap) aa_panic(a, "prologue too large"); + words[n++] = aa64_stp64_soff(AA_FP, AA_LR, AA_TMP1, 0); + } + /* x29 = sp + os (the anchor). Same SP-as-Rn restriction: build via immediate + * copy + register add for a far os. */ + if (aa64_addsub_imm_fits(os, &imm12, &sh)) { + if (n >= cap) aa_panic(a, "prologue too large"); + words[n++] = aa64_add_imm(1, AA_FP, AA_SP, imm12, sh); + } else { + aa_words_load_imm(a, words, cap, &n, AA_TMP0, os); + if (n + 2u > cap) aa_panic(a, "prologue too large"); + words[n++] = aa64_add_imm(1, AA_FP, AA_SP, 0, 0); + words[n++] = aa64_add(1, AA_FP, AA_FP, AA_TMP0); + } aa_words_callee_saves(a, 1, words, cap, &n); return n; } @@ -1717,18 +1837,13 @@ static u32 aa_build_prologue_words(AANativeTarget* a, const AAFrameLayout* L, aa_words_stack_probe(a, words, cap, &n, L->frame_size, interval); } aa_words_sub_sp_frame(a, words, cap, &n, L->frame_size); - if (a->slim_small_frame) { - /* `stp x29, x30, [sp, #saved_pair_off]` — skip the `add x17, sp, #...` - * scratch the fat path needs. Valid when the offset fits stp's - * signed-7-bit scaled immediate (saved_pair_off <= 504). */ - u32 save_off = aa_sp_off_saved_pair(L); - if (n >= cap) aa_panic(a, "prologue too large"); - words[n++] = aa64_stp64_soff(AA_FP, AA_LR, AA_SP, (i32)(save_off / 8u)); - } else { - aa_words_saved_pair_addr(a, words, cap, &n, L); - if (n >= cap) aa_panic(a, "prologue too large"); - words[n++] = aa64_stp64_soff(AA_FP, AA_LR, AA_TMP1, 0); /* fp,lr @ [x17] */ - } + /* Fat top-record prologue: build the saved-pair address (sp + saved_pair_off) + * into x17, store the pair there, then anchor fp. Reached only by the + * single-pass (-O0) path and the Windows-variadic top_home case — the known- + * frame -O1 path takes slim_prologue or the fp_at_bottom layout above. */ + aa_words_saved_pair_addr(a, words, cap, &n, L); + if (n >= cap) aa_panic(a, "prologue too large"); + words[n++] = aa64_stp64_soff(AA_FP, AA_LR, AA_TMP1, 0); /* fp,lr @ [x17] */ aa_words_frame_ptr_from_sp(a, words, cap, &n, L); /* Save callee-saved registers the allocator used (fp-relative; their slots * were reserved first by aa_reserve_callee_saves so offsets fit stur). */ @@ -2063,31 +2178,27 @@ static void aa_func_begin_known_frame(NativeTarget* t, const CGFuncDesc* fd, } /* Frame is final: slot_bytes (cum_off) and out_stack (max_outgoing) are both * known, so the prologue immediates and slim-form choice are settled here. - * frame_size_final must be set before aa_build_prologue_words / entry saves, - * since the bottom-record offset helpers read it. */ + * frame_size_final + out_stack_final must be set before aa_build_prologue_words + * / entry saves, since the bottom-record offset helpers read both. */ L = aa_build_layout(a->frame.cum_off, a->frame.max_outgoing, a->top_home_bytes); a->frame_size_final = L.frame_size; - /* Slim Tier A: no callee-saves, no alloca, no body slots, no outgoing stack - * args — the whole frame is the 16-byte record. fp_at_bottom: a small frame - * with callee-saves/locals and no outgoing stack args; the record moves to - * the bottom (fp = sp) so sp adjustment folds into the pre/post-indexed - * stp/ldp (frame_size <= 504 keeps the post-index ldp imm in range). - * Otherwise slim_small_frame keeps the top-record layout but skips the - * x17/x10 scratch (out_stack>0 small frames land here). A Windows-variadic - * home area forces the fat top-record layout: it lives above the saved pair, - * which neither the slim forms (saved pair at the very top) nor the - * bottom-record (saved pair at the very bottom) leave room for. (See - * aa_func_end for the single-pass path, which never takes any slim form.) */ + a->out_stack_final = L.out_stack; + /* Layout choice (W1.1). Slim Tier A: no callee-saves, no alloca, no body + * slots, no outgoing stack args — the whole frame is the 16-byte record, with + * its own folded `stp [sp,#-16]!` encoding. Otherwise the uniform x29-at-bottom + * (fp_at_bottom) layout: x29 anchored out_stack bytes above sp, every slot a + * positive [x29,#k], for ALL remaining known frames (out_stack>0, alloca, and + * arbitrarily large frames included). The only exception is a Windows-variadic + * GP home area, which must sit ABOVE the saved pair (the plain-pointer va_list + * walks register then stack varargs as one contiguous block) — bottom-record + * has no room for it, so that case keeps the fat top-record layout. + * (See aa_func_end for the single-pass path, which never takes any slim/bottom + * form.) */ a->slim_prologue = a->frame.ncallee_saves == 0 && !a->frame.has_alloca && L.slot_bytes == 0 && L.out_stack == 0 && !a->top_home_bytes; - a->fp_at_bottom = !a->slim_prologue && !a->frame.has_alloca && - L.out_stack == 0 && L.frame_size <= 504u && - !a->top_home_bytes; - a->slim_small_frame = !a->slim_prologue && !a->fp_at_bottom && - !a->frame.has_alloca && !a->top_home_bytes && - aa_sp_off_saved_pair(&L) <= 504u; + a->fp_at_bottom = !a->slim_prologue && !a->top_home_bytes; n = aa_build_prologue_words(a, &L, words, AA_PROLOGUE_WORDS); for (u32 i = 0; i < n; ++i) aa_emit32(t->mc, words[i]); a->minimal_prologue_words = n; diff --git a/test/opt/aa64_tail_call.sh b/test/opt/aa64_tail_call.sh @@ -50,13 +50,36 @@ if ! grep -Eq '\bb\b.*target.*AARCH64_JUMP26' "$WORK/direct_stack.caller.dis"; t fail 'direct stack-arg tail missing b target / AARCH64_JUMP26' \ "$WORK/direct_stack.caller.dis" fi -if ! grep -Eq '\bstr\b.*\[x29, #16\]' "$WORK/direct_stack.caller.dis" || - ! grep -Eq '\bstr\b.*\[x29, #24\]' "$WORK/direct_stack.caller.dis"; then +# The tail's two stack-arg stores must land in the caller's incoming stack-arg +# window -- the same [x29,#K] / [x29,#K+8] addresses the caller LOADS its own +# 9th/10th incoming args from (physically the caller's CFA window). With the +# W1.1 uniform x29-at-bottom layout that window is at the CFA-relative offset +# x29 + (frame_size - out_stack), not the old top-record fixed [x29,#16]/[#24]; +# pinning the literal offset would be brittle, so assert that some [x29,#K] is +# both an incoming-arg load and a tail-arg store, with [x29,#K+8] the same. +if ! awk ' + function off(s, t) { + if (!match(s, /#[0-9]+\]/)) return -1 + t = substr(s, RSTART + 1, RLENGTH - 2) + return t + 0 + } + /ldr[[:space:]]+x[0-9]+, \[x29, #[0-9]+\]/ { loaded[off($0)] = 1 } + /str[[:space:]]+x[0-9]+, \[x29, #[0-9]+\]/ { stored[off($0)] = 1 } + END { + for (k in stored) + if (loaded[k] && stored[k+8] && loaded[k+8]) { ok = 1 } + exit(ok ? 0 : 1) + } +' "$WORK/direct_stack.caller.dis"; then fail 'direct stack-arg tail did not write caller incoming stack window' \ "$WORK/direct_stack.caller.dis" fi +# The caller frame must be torn down before the sibling branch. The W1.1 general +# bottom-record teardown recovers sp from the x29 anchor (`add sp, x16, #...`); +# the os==0 small-frame fold uses a post-indexed `ldp ...,[sp],#N`. Accept either. if ! awk ' - /add[[:space:]]+sp, sp,/ { restored = 1 } + /add[[:space:]]+sp, (sp|x1[0-9]),/ { restored = 1 } + /ldp[[:space:]]+x29, x30, \[sp\], #/ { restored = 1 } /[[:space:]]b[[:space:]]+.*target.*AARCH64_JUMP26/ { found = 1; ok = restored; exit } diff --git a/test/opt/aa64_x29_bottom.sh b/test/opt/aa64_x29_bottom.sh @@ -0,0 +1,152 @@ +#!/usr/bin/env bash +# Structural checks for the -O1 aa64 uniform x29-at-bottom known-frame layout +# (doc/plan/O1.md W1.1). +# +# At -O1 the frame is fully known before the body emits, so aa64 anchors x29 at +# the BOTTOM of the static slots (just above the outgoing-arg area). Every spill +# / local slot is then a one-instruction POSITIVE `ldr/str [x29,#k]` within the +# 32 KB scaled reach (`add x16,x29,#hi ; ldr [x16,#lo]` only past it). This +# replaces the old `sub x17,x29,#k ; ldur` two-instruction fallback that was 36% +# of lvm / 12% of sqlite instructions. +# +# The saved x29/x30 pair sits AT [x29]/[x29+8] (the frame-pointer chain kit's +# unwinder / __kit_backtrace walks) and outgoing args stay sp-relative +# ([sp,#k]) so calls after an alloca still address their arg area at current sp. +# +# These checks pin the resulting disassembly on aarch64 (the reference backend): +# 1. a big-frame, high-pressure function spills via `ldr/str [x29,#k]` and +# emits NO `sub xN,x29,#k` for spill addressing; +# 2. an alloca function (sp floats) still addresses spills via [x29,#k] and +# never recomputes a slot address with `sub xN,x29,#k`; +# 3. a >32 KB-frame function uses the add-build (`add x16,x29,#... ; ldr/str`) +# and never `sub xN,x29,#`; +# 4. a signed-narrow-load function never uses `sub xN,x29,#` for spills. +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +KIT="${KIT:-$ROOT/build/kit}" +WORK="$ROOT/build/test/opt/aa64_x29_bottom" +rm -rf "$WORK" +mkdir -p "$WORK" + +fail() { + printf 'aa64-x29-bottom check FAILED: %s\n' "$1" >&2 + if [ -n "${2:-}" ] && [ -f "$2" ]; then + sed 's/^/ | /' "$2" >&2 + fi + exit 1 +} + +slice_func() { + local src="$1" func="$2" out="$3" + awk -v name="$func" ' + $0 ~ "^[0-9a-f]+ <" name ">:" { in_fn = 1; print; next } + /^[0-9a-f]+ </ { in_fn = 0 } + in_fn { print } + ' "$src" > "$out" +} + +compile_case() { + local name="$1" src="$2" + "$KIT" cc -target aarch64-linux-gnu -O1 -std=c11 -c "$src" \ + -o "$WORK/$name.o" > "$WORK/$name.cc.out" 2> "$WORK/$name.cc.err" || + { cat "$WORK/$name.cc.err" >&2; fail "compile of $name failed"; } + "$KIT" objdump -d "$WORK/$name.o" \ + > "$WORK/$name.dis" 2> "$WORK/$name.objdump.err" +} + +# A spill never recomputes its address with `sub xN, x29, #k` on the known-frame +# path -- that is exactly the 2-insn pattern W1.1 removes. +no_frame_sub() { # $1 = sliced fn file, $2 = label + if grep -Eq 'sub[[:space:]]+x[0-9]+, x29, #' "$1"; then + fail "$2 recomputes a frame-slot address with 'sub xN, x29, #' (W1.1 should address [x29,#k] positively)" "$1" + fi +} + +# ---- fixtures ---- + +# 1. Big frame (>256-byte spill area) + outgoing stack args (>8-arg calls). This +# is the case W1.1 targets: a large frame means spill offsets escape `ldur`'s +# +-256 range, and out_stack>0 disqualifies the old folded bottom-record. The +# baseline emits `sub x17,x29,#k ; ldur` for each such spill; x29-at-bottom +# must address every one as a positive [x29,#k] with no frame-address `sub`. +cat > "$WORK/pressure.c" <<'EOF' +extern long sink(long,long,long,long,long,long,long,long,long,long); +long pressure(long *p) { + long v[48]; + for (int i = 0; i < 48; i++) v[i] = p[i] + i; + long s = 0; + s += sink(v[0],v[1],v[2],v[3],v[4],v[5],v[6],v[7],v[8],v[9]); + s += sink(v[10],v[11],v[12],v[13],v[14],v[15],v[16],v[17],v[18],v[19]); + s += sink(v[20],v[21],v[22],v[23],v[24],v[25],v[26],v[27],v[28],v[29]); + s += sink(v[30],v[31],v[32],v[33],v[34],v[35],v[36],v[37],v[38],v[39]); + for (int i = 0; i < 48; i++) s += v[i]; + return s; +} +EOF +compile_case pressure "$WORK/pressure.c" +slice_func "$WORK/pressure.dis" pressure "$WORK/pressure.fn" +[ -s "$WORK/pressure.fn" ] || fail "pressure not found in disassembly" "$WORK/pressure.dis" +no_frame_sub "$WORK/pressure.fn" pressure +grep -Eq '(ldr|str)[[:space:]]+[wx][0-9]+, \[x29, #[0-9]+\]' "$WORK/pressure.fn" || + fail "pressure has no positive [x29,#k] spill access (expected x29-at-bottom)" "$WORK/pressure.fn" +# The prologue must anchor x29 = sp + os (saved fp/lr pair at [x29]/[x29+8]). +grep -Eq 'add[[:space:]]+x29, sp' "$WORK/pressure.fn" || + fail "pressure prologue did not anchor x29 = sp + os" "$WORK/pressure.fn" + +# 2. alloca: sp floats during the body, but x29 stays put, so spills are still +# [x29,#k] and outgoing args are [sp,#k]. (The call below takes stack args.) +cat > "$WORK/alloca_fn.c" <<'EOF' +extern long use(long *, long, long, long, long, long, long, long, long, long); +long alloca_fn(long n, long x) { + long *p = __builtin_alloca(n * sizeof(long)); + for (long i = 0; i < n; i++) p[i] = x + i; + long r = use(p, x, x+1, x+2, x+3, x+4, x+5, x+6, x+7, x+8); + return r + p[0]; +} +EOF +compile_case alloca_fn "$WORK/alloca_fn.c" +slice_func "$WORK/alloca_fn.dis" alloca_fn "$WORK/alloca_fn.fn" +[ -s "$WORK/alloca_fn.fn" ] || fail "alloca_fn not found in disassembly" "$WORK/alloca_fn.dis" +no_frame_sub "$WORK/alloca_fn.fn" alloca_fn +grep -Eq 'add[[:space:]]+x29, sp' "$WORK/alloca_fn.fn" || + fail "alloca_fn prologue did not anchor x29 = sp + os" "$WORK/alloca_fn.fn" + +# 3. >32 KB frame: the scaled `ldr/str [x29,#k]` reach is exceeded, so the slot +# address is built with `add x16/x17, x29, #hi` -- never `sub xN, x29, #k`. +cat > "$WORK/huge.c" <<'EOF' +extern void use_buf(volatile char *, volatile char *); +long huge(long x) { + volatile char a[40000]; + volatile char b[8]; + a[0] = (char)x; + b[0] = (char)(x + 1); + use_buf(a, b); + return a[0] + b[0]; +} +EOF +compile_case huge "$WORK/huge.c" +slice_func "$WORK/huge.dis" huge "$WORK/huge.fn" +[ -s "$WORK/huge.fn" ] || fail "huge not found in disassembly" "$WORK/huge.dis" +no_frame_sub "$WORK/huge.fn" huge +grep -Eq 'add[[:space:]]+x1[67], x29, #' "$WORK/huge.fn" || + fail "huge >32KB frame did not build a far slot address with 'add x16/x17, x29, #'" "$WORK/huge.fn" + +# 4. signed narrow loads in a big frame: spilled narrow values still address +# [x29,#k] (ldrsb when reloaded as signed), never a sub-x29 recompute. +cat > "$WORK/snl.c" <<'EOF' +extern long sink2(long,long,long,long,long,long,long,long,long,long); +long snl(signed char *p) { + signed char v[400]; + for (int i = 0; i < 400; i++) v[i] = p[i]; + long s = sink2(v[0],v[40],v[80],v[120],v[160],v[200],v[240],v[280],v[320],v[360]); + for (int i = 0; i < 400; i += 17) s += v[i]; + return s; +} +EOF +compile_case snl "$WORK/snl.c" +slice_func "$WORK/snl.dis" snl "$WORK/snl.fn" +[ -s "$WORK/snl.fn" ] || fail "snl not found in disassembly" "$WORK/snl.dis" +no_frame_sub "$WORK/snl.fn" snl + +printf 'aa64-x29-bottom: ok (pressure, alloca, >32KB add-build, signed narrow)\n' diff --git a/test/opt/redundant_frame_sub.sh b/test/opt/redundant_frame_sub.sh @@ -2,15 +2,17 @@ # Structural check for the -O1 local frame-address `sub`-CSE (O1.md W1a). # # An `IR_ADDR_OF` of a frame-local materializes a frame-slot address into a -# register; on aarch64 that lowers to `sub xN, x29, #k` (the offset built off the -# frame base). Two back-to-back addr-of's of the *same* local recompute the -# identical `sub xN, x29, #k`. The W1a peephole in src/opt/pass_combine.c -# (mir_combine) tracks the still-live earlier producer and rewrites the -# recompute into a register move (`mov xN, xM`), so the second `sub` vanishes. +# register; under the W1.1 x29-at-bottom layout aarch64 builds it as a positive +# `add xN, x29, #k` off the frame base. Two back-to-back addr-of's of the *same* +# local recompute the identical `add xN, x29, #k`. The W1a peephole in +# src/opt/pass_combine.c (mir_combine) tracks the still-live earlier producer and +# rewrites the recompute into a register move (`mov xN, xM`), so the second +# address build vanishes. (Before W1.1 the build was a negative `sub xN,x29,#k`; +# W1.1 made frame addressing positive, but the W1a CSE still applies to the add.) # # This pins the resulting disassembly on aarch64 (the reference backend), where # the pattern has a stable mnemonic. Each local is addressed twice as adjacent -# call arguments; with the peephole only ONE `sub xN, x29, #k` survives per +# call arguments; with the peephole only ONE `add xN, x29, #k` survives per # distinct constant K, and the duplicate becomes a `mov`. set -euo pipefail @@ -55,16 +57,16 @@ B="$(fn_body _g)" [ -n "$B" ] || B="$(fn_body g)" [ -n "$B" ] || fail "g not found in disassembly" -# For each distinct constant K appearing in `sub xN, x29, #K`, the address is +# For each distinct constant K appearing in `add xN, x29, #K`, the address is # built exactly once; a back-to-back recompute of the same K must NOT survive. # Collect the K constants and assert no duplicates. -KS="$(printf '%s\n' "$B" | grep -oE 'sub[[:space:]]+x[0-9]+, x29, #[0-9]+' \ +KS="$(printf '%s\n' "$B" | grep -oE 'add[[:space:]]+x[0-9]+, x29, #[0-9]+' \ | grep -oE '#[0-9]+$' | sort)" -[ -n "$KS" ] || fail "expected at least one 'sub xN, x29, #k' frame-address build" +[ -n "$KS" ] || fail "expected at least one 'add xN, x29, #k' frame-address build" DUP="$(printf '%s\n' "$KS" | uniq -d || true)" if [ -n "$DUP" ]; then - fail "duplicate frame-address build survived (sub xN, x29, #k recomputed for: $DUP)" + fail "duplicate frame-address build survived (add xN, x29, #k recomputed for: $DUP)" fi # Sanity: the de-duplicated address must be reused via a register move, so the @@ -74,4 +76,4 @@ if ! printf '%s\n' "$B" | grep -Eq 'mov[[:space:]]+x[0-9]+, x[0-9]+'; then fail "expected a 'mov xN, xM' reusing the CSE'd frame address" fi -printf 'redundant_frame_sub: OK (no duplicate sub xN,x29,#k; address reused via mov)\n' +printf 'redundant_frame_sub: OK (no duplicate add xN,x29,#k; address reused via mov)\n'