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