Size-aware callee-saved ordering and sunk-cost register tiebreak - #2109
Merged
Conversation
steveklabnik
marked this pull request as ready for review
August 5, 2026 09:49
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
force-pushed
the
claude/incremental-recompile-4w4oah
branch
from
August 5, 2026 09:51
2ae5dd4 to
24f59a1
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
#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_REGSis 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 grewlifeby 48 bytes when an aggregate base landed on R12 (+115 bytes in one function).The sunk-cost tiebreak.
pick_free_registernow 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_rangesthat 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