Skip to content

Size-aware callee-saved ordering and sunk-cost register tiebreak - #2109

Merged
steveklabnik merged 1 commit into
trunkfrom
claude/incremental-recompile-4w4oah
Aug 5, 2026
Merged

Size-aware callee-saved ordering and sunk-cost register tiebreak#2109
steveklabnik merged 1 commit into
trunkfrom
claude/incremental-recompile-4w4oah

Conversation

@steveklabnik

Copy link
Copy Markdown
Collaborator

#2101's caller-saved-first policy always picked R11 on x86-64, and every R11-addressed instruction pays a REX byte — measured at +112 bytes across the example corpus, concentrated in arithmetic-heavy code. This recovers it and more, without giving back any caller-saved win.

Encoding-cost ordering. CALLEE_SAVED_REGS is now [Rbx, R13, R14, R15, R12]: Rbx leads as the only legacy register (no REX on byte/dword forms, shorter push/pop), and R12 trails because its low three bits are RSP's — every memory operand it bases needs a SIB byte. The R12 penalty was measured, not guessed: an intermediate [Rbx, R12, …] ordering grew life by 48 bytes when an aggregate base landed on R12 (+115 bytes in one function).

The sunk-cost tiebreak. pick_free_register now tries a compact callee-saved register whose prologue save is already paid → caller-saved → fresh callee-saved. Since a one-pass rule can't know the final save set, assignment runs twice: pass 1 is exactly the existing policy, and if it commits no callee-saved register, pass 2 is skipped outright — push-free functions stay push-free structurally, not empirically. The reuse pass is accepted only if it adds no save, houses no fewer values in registers, and spills no more slots; both passes are complete valid allocations, so no repair step and no interference rewriting. A one-pass "prefer any already-used callee-saved" variant was measured and rejected (+272 bytes — it prefers R12, same REX as R11).

Results: 22-program corpus −912 bytes (18 smaller, 4 unchanged, 0 larger; sudoku −352), landing 800 bytes below the pre-#2101 baseline. Push/pop counts and total asm line counts identical on every program — zero saves reintroduced, zero added spills. AArch64 declares no compact registers (fixed-width encoding prices all registers alike, so pass 2 never runs) and its output is byte-identical to trunk across 44 asm dumps and 22 linked binaries. Honest cost: compiles pay ~2–3% where the second pass runs (sudoku 0.373→0.386 s).

Also adds the load-bearing comment on build_live_ranges that ranges are textual intervals, not liveness — and why any future refinement needs the #2107 two-part treatment.

Fixes RUE-1227.

Validation

Premerge (78/78, final tree), codegen units 662 (+3: tiebreak fires, push-free stays push-free with pass 2 skipped, guard rejects a save-adding reuse pass), cfg 243, quick (re-run post-rebase), CLI abi/borrow, differential-oracle targets, reproducible-programs, clippy, fmt.


Generated by Claude Code

@steveklabnik
steveklabnik marked this pull request as ready for review August 5, 2026 09:49
@steveklabnik
steveklabnik enabled auto-merge August 5, 2026 09:49
The callee-saved class is now ordered by encoding cost -- Rbx leads as
the only legacy register (no REX on byte and dword forms, shorter
push/pop) and R12 trails because its low bits are Rsp's, so every
memory operand it bases needs a SIB byte, a penalty measured live when
an aggregate base moved onto it. On top of the ordering, pick_free
prefers a compact callee-saved register whose save is already sunk over
a fresh caller-saved one. Assignment runs twice to know the final save
set: pass one is the existing policy, pass two reuses its sunk saves,
and a function that commits no callee-saved register skips pass two
outright, so push-free functions stay push-free structurally. The
reuse pass is taken only when it adds no save, houses no fewer values,
and spills no more slots. AArch64 declares no compact registers --
fixed-width encoding prices every register alike -- and its output is
byte-identical. The example corpus shrinks 912 bytes with push and pop
counts identical on every program; compiles pay two to three percent
where the second pass runs. Also documents on build_live_ranges that
ranges are textual intervals, not liveness.
@steveklabnik
steveklabnik force-pushed the claude/incremental-recompile-4w4oah branch from 2ae5dd4 to 24f59a1 Compare August 5, 2026 09:51
@steveklabnik
steveklabnik added this pull request to the merge queue Aug 5, 2026
Merged via the queue into trunk with commit 5a96e9a Aug 5, 2026
22 checks passed
@steveklabnik
steveklabnik deleted the claude/incremental-recompile-4w4oah branch August 5, 2026 10:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants