Skip to content

Give spilled bundles a register in the loops where they are used - #270

Open
EvanCarroll wants to merge 2 commits into
bytecodealliance:mainfrom
EvanCarroll:trial-fallback-1
Open

EvanCarroll wants to merge 2 commits into
bytecodealliance:mainfrom
EvanCarroll:trial-fallback-1

Conversation

@EvanCarroll

Copy link
Copy Markdown

Summary

When a spill bundle that is live across a call fails the whole-range second
chance, take the loops where its virtual registers are used — outermost first —
and try to give each piece in the loop one register via a zero-cost probe (no
evictions). If the loop region contains a call, only non-preferred registers
are probed since the call clobbers preferred ones anyway. Pieces crossing a
point that clobbers every allocatable register, or whose vreg has no use in the
loop, stay on the stack.

Once a region fits, each vreg's pieces are kept in a register only if:

  • the value is already in its spill slot on every path into the loop, or
  • it would be stored back at no more than one loop exit (counting
    block-parameter edges to targets outside the loop).

Otherwise the pieces go back to the stack. Every piece of a vreg that is in its
spill slot on entry is flagged, and move insertion skips the redundant store
into that slot at loop exits and on block-parameter edges.

This fixes #268 at MAX_SPLITS_PER_SPILLSET = 2: the issue's inner loop (two
vpmaddwd instructions) goes from 2 stack references to 0, with no increase
to the split limit.

Results vs limit 2 (origin/main 4a23344)

criterion target result verdict
Issue function inner loop no spill 0 stack refs (limit2: 2) pass
Kernel wasm_i32_4x1 wall-clock (median μs, 512/2048/512×2048) ≥ limit 4 28.8 / 115.1 / 98.7 (limit2: 32.8 / 131.3 / 115.7) pass
Execution wall-clock (cycles), 33 benchmarks neutral -0.12% geomean (stable benchmarks); no significant regression pass
Execution instructions (callgrind), 33 benchmarks neutral -0.016% geomean pass
Pathological inputs (extract / example) ≤ +1% +0.16% / +0.71% pass
Keccak stack stores / loads / bytes ≤ +0.5% each +0.00% / -0.11% / +0.01% pass
Tinygo stack stores / loads / bytes ≤ +0.5% each -0.41% / -0.71% / +0.13% pass
Correctness all pass cargo test, fuzzer (186k runs), filetests (1378), wast simd (158), checker harness (35k functions, seeds 41–46) pass

How it works

  1. Trigger: after try_allocating_regs_for_spilled_bundles, for each spill
    bundle that is live across a call.
  2. Loop iteration: for each loop (outermost first) where the bundle's vregs
    are used, collect the bundle's pieces that fall inside the loop.
  3. Probe: try to allocate the collected pieces to a single register with
    zero eviction cost. In regions containing a call, only non-preferred
    registers are probed.
  4. Keep-or-discard: after a successful probe, check each vreg's pieces. A
    value defined inside the loop is kept only if it exits by at most one edge;
    a value live into the loop is kept and flagged as already in its spill slot
    if every predecessor outside the loop stores it there.
  5. Move insertion: the existing move-insertion pass checks the in-spill-slot
    flag and skips stores that would write a value back to a slot that already
    holds it.

Correctness

  • regalloc2 cargo test
  • Random-input checker harness: 34,879 functions over seeds 41–46 at 60,000
    cases each; 15,682 loop regions allocated, 10,091 stores skipped, all clean
  • libFuzzer ion: 10 min × 6 workers (186k runs, no crash)
  • Cranelift filetests: 1,378 pass, no re-bless
  • Wasmtime cargo test --test wast -- simd: 158 pass

… instructions

Make the fuzz generator emit instructions that clobber nearly every
register of a class, the way a call does, so that spill bundles in
the fuzzer-generated functions actually reach the loop-region second
chance pass. One register per operand of the clobbered class stays
free at the top of the file so that every preferred register is
clobbered, keeping the instruction itself allocatable.
When a spill bundle that is live across a call fails the whole-range
second chance, take the loops where its vregs are used, outermost
first, and try to give each piece in the loop one register via a
zero-cost probe. If the region contains a call, only non-preferred
registers are probed. Pieces crossing a full clobber point or whose
vreg has no use in the loop stay on the stack.

After a successful probe, a vreg's pieces are kept only if its value
is already in the spill slot on every way into the loop or it would
be stored back at no more than one loop exit. Move insertion skips
the store into a slot that already holds the value at loop exits and
on block-parameter edges.
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.

A bundle split twice around call clobbers spills in the call-free loops it spans

1 participant