kit

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

commit 277edfa3d286c43fc3e80ffc1f0ad0c5795321d3
parent 77852d159a7e48ca536dbc560544eaa5114a1a84
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Tue, 16 Jun 2026 08:20:13 -0700

opt(aa64): frame elision on no-spill leaf functions (O1-PATTERNS L10)

A no-spill leaf -- no call (x30/LR never clobbered), no callee-saves, no
alloca, no body slots, no outgoing stack args, no inline asm, no frame-chain
read (__builtin_frame_address/return_address), and no INCOMING stack args
(addressed fp-relative via x29) -- needs no frame record at all. Add a
per-function 'frameless' flag, decided at frame-finalize from the existing
NativeKnownFrameDesc is_leaf/has_asm/reads_frame signals plus
aa_signature_stack_bytes==0; when set, the prologue/epilogue emit nothing but
ret and the CFA stays at sp with the return address live in LR (the aa64 CIE
default, so no saved fp/lr rules). x29 is never written, so it still holds the
caller's fp and the backtrace fp-chain walks straight through.

Mirrors the rv64 frameless-leaf tier. Implies slim_prologue=0/fp_at_bottom=0.
A one-op accessor (add/getfield/sum3, fadd) now drops its stp/add/ldp frame;
non-leaf / addr-taken / recursion / varargs / asm / incoming-stack-arg
functions keep theirs. Density: lapi -2.0%, tinyexpr -1.4%, lparser/yyjson
-0.65%. Guards: test/opt/o1p_aa64.sh (L10) + updated prologue_tier.sh aa64
characterization (frameless leaf + non-leaf/asm keep-frame guards).

Diffstat:
Msrc/arch/aa64/native.c | 48++++++++++++++++++++++++++++++++++++++++++++++--
Mtest/opt/prologue_tier.sh | 22++++++++++++++++++++--
2 files changed, 66 insertions(+), 4 deletions(-)

diff --git a/src/arch/aa64/native.c b/src/arch/aa64/native.c @@ -312,6 +312,17 @@ typedef struct AANativeTarget { * 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; + /* L10: a stricter case of slim Tier A — a true frameless leaf. Set by + * aa_func_begin_known_frame when, on top of the slim conditions (no + * callee-saves, no alloca, no body slots, no outgoing stack args, no + * sret/variadic), the function is also a LEAF (no call of any kind, so x30/LR + * is never clobbered), contains no inline asm (which could clobber LR or make + * a call opaquely), and never reads its own frame chain + * (__builtin_frame_address / __builtin_return_address). Such a function needs + * no frame record at all: the prologue/epilogue emit nothing but `ret`, and + * the CFA stays at sp with the return address live in LR (the aa64 CIE + * default). Implies slim_prologue=0 and fp_at_bottom=0. */ + u8 frameless; /* 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 @@ -1322,6 +1333,7 @@ static void aa_func_begin_common(NativeTarget* t, const CGFuncDesc* fd) { a->nalloca = 0; a->slim_prologue = 0; a->fp_at_bottom = 0; + a->frameless = 0; a->frame_size_final = 0; a->out_stack_final = 0; a->slot_sp_base = 0; @@ -1587,6 +1599,9 @@ static u32 aa_frame_base_save_off(AANativeTarget* a, const AAFrameLayout* L) { static void aa_words_restore_frame(AANativeTarget* a, u32* words, u32 cap, u32* n, const AAFrameLayout* L) { + /* L10: frameless leaf — nothing to tear down (no record, sp untouched). The + * caller emits the bare `ret`. */ + if (a->frameless) return; 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 @@ -1755,6 +1770,10 @@ static void aa_words_callee_saves(AANativeTarget* a, int save, u32* words, static u32 aa_build_prologue_words(AANativeTarget* a, const AAFrameLayout* L, u32* words, u32 cap) { u32 n = 0; + /* L10: frameless leaf — no record, no sp adjustment. The body uses only + * allocated registers + incoming args; sp is untouched and LR holds the live + * return address (no call clobbered it). */ + if (a->frameless) return 0; if (!L->frame_size) return 0; if (a->slim_prologue) { if (cap < 2u) aa_panic(a, "prologue too large"); @@ -2040,7 +2059,13 @@ static void aa_func_end(NativeTarget* t) { aa_patch_prologue(a, &L); aa_apply_patches(a, &L); } - { + if (a->frameless) { + /* L10: frameless leaf — CFA = sp (unchanged from entry) and the return + * address stays live in LR (the aa64 CIE default), so no saved-register + * rules. The state holds from the first instruction (offset 0). */ + mc_cfi_set_next_pc_offset(mc, 0); + mc_cfi_def_cfa(mc, AA_SP, 0); + } else { i32 cfa = aa_cfa_off(a); mc_cfi_set_next_pc_offset(mc, prologue_advance_words * 4u); /* CFA = caller's sp, an fp-relative offset that depends on the layout: @@ -2135,6 +2160,9 @@ static u32 aa_known_callee_saves(NativeTarget* t, return ncls; } +static u32 aa_signature_stack_bytes(NativeTarget* t, KitCgTypeId fn_type, + int* variadic, u32* nparams); + /* Optimizer entry point: the full frame is supplied up front, so the prologue, * entry saves, slim-form eligibility, allocas, and tail epilogues are all final * the moment they are emitted — no back-patching (aa_func_end skips the patch @@ -2198,7 +2226,23 @@ static void aa_func_begin_known_frame(NativeTarget* t, const CGFuncDesc* fd, 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->top_home_bytes; + /* L10: a slim Tier A function that is additionally a leaf (no call clobbers + * x30/LR), has no inline asm (which could clobber LR / call opaquely), and + * never reads its own frame chain (__builtin_frame_address / + * __builtin_return_address) needs no frame record at all. Emit no prologue / + * epilogue / saved-pair CFI; the return address rides LR and the CFA stays at + * sp. A frameless function has frame_size 0 in this layout (slim's whole frame + * is the 16-byte record we are eliding), so it takes neither slim nor + * fp_at_bottom encoding. Additionally exclude any function with incoming + * stack arguments: those are addressed fp-relative (aa_fp_off_in_arg, off + * x29), so a function that reads them needs the x29 anchor a frameless + * prologue never sets up. (Stack args do not show up in L.slot_bytes / + * L.out_stack — they sit above the saved pair — so this guard is separate.) */ + a->frameless = a->slim_prologue && frame && frame->is_leaf && + !frame->has_asm && !frame->reads_frame && + aa_signature_stack_bytes(t, fd->fn_type, NULL, NULL) == 0u; + if (a->frameless) a->slim_prologue = 0; + a->fp_at_bottom = !a->slim_prologue && !a->frameless && !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/prologue_tier.sh b/test/opt/prologue_tier.sh @@ -82,10 +82,28 @@ int asm_locals(int x) { EOF # ===================== aa64 reference (characterization) ===================== +# L10 (O1-PATTERNS): a no-spill LEAF (no call, no callee-saves, no slots, no +# outgoing args, no asm, no frame-chain read) is now FRAMELESS -- it emits no +# `stp x29,x30` record at all, just the op + `ret`. A non-leaf (calls g) must +# still establish the frame record so x30/LR survives the call. compile_case aarch64-linux-gnu aa64_leaf "$WORK/leaf.c" slice_func "$WORK/aa64_leaf.dis" leaf "$WORK/aa64_leaf.fn" -grep -Eq 'stp[[:space:]]+x29, x30, \[sp, #-16\]!' "$WORK/aa64_leaf.fn" || - fail 'aa64 leaf is not using the slim_prologue frame record' "$WORK/aa64_leaf.fn" +grep -Eq 'stp[[:space:]]+x29, x30' "$WORK/aa64_leaf.fn" && + fail 'aa64 no-spill leaf still emits a frame record (L10 should elide it)' "$WORK/aa64_leaf.fn" +grep -Eq '\bret\b' "$WORK/aa64_leaf.fn" || + fail 'aa64 frameless leaf is missing its ret' "$WORK/aa64_leaf.fn" + +# Guard: a non-leaf (calls g) must KEEP the frame so x30/LR survives the call. +compile_case aarch64-linux-gnu aa64_leaf_call "$WORK/leaf_call.c" +slice_func "$WORK/aa64_leaf_call.dis" leaf_call "$WORK/aa64_leaf_call.fn" +grep -Eq 'stp[[:space:]]+x29, x30' "$WORK/aa64_leaf_call.fn" || + fail 'aa64 non-leaf dropped the frame record (L10 over-fired across a call)' "$WORK/aa64_leaf_call.fn" + +# Guard: inline asm may clobber x30/LR, so a leaf with asm must KEEP the frame. +compile_case aarch64-linux-gnu aa64_asm "$WORK/asm_fn.c" +slice_func "$WORK/aa64_asm.dis" asm_fn "$WORK/aa64_asm.fn" +grep -Eq 'stp[[:space:]]+x29, x30' "$WORK/aa64_asm.fn" || + fail 'aa64 leaf with inline asm dropped the frame record (frameless must not fire with asm)' "$WORK/aa64_asm.fn" # ===================== x64 slim (no sub rsp on empty frame) ================== compile_case x86_64-linux-gnu x64_leaf "$WORK/leaf.c"