Skip to content

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

Open

Description

@EvanCarroll

Summary

With MAX_SPLITS_PER_SPILLSET = 2, a bundle that needs a few splits to get
around call clobbers reaches the limit and falls back to
split_into_minimal_bundles. That puts every gap between uses into the spill
bundle. The spill bundle covers the original bundle's whole range, calls
included, so it gets a stack slot. A value that is live across calls in an outer
loop, and updated in a call-free inner loop, is then loaded and stored on every
iteration of the inner loop, with free registers available.

Reproducer

spill-in-loop.clif (attached, 1220 lines): a real function, tract's
wasm_i32_4x1 int8 matrix-vector kernel, emitted by wasmtime 49
(wasmtime compile --emit-clif) with an x86_64 AVX2/AVX512 target header added.
Run it on Cranelift main (dcfbea188d), which uses regalloc2 0.15.2:

CRANELIFT_TEST_BLESS=1 clif-util test spill-in-loop.clif

The inner loop (block58 in the CLIF) has two vector accumulators. The first
stays in a register; the second goes through the stack:

vpmaddwd %xmm0, %xmm4, %xmm0
vpaddd   %xmm1, %xmm0, %xmm1        ; first accumulator, in a register
...
vpmaddwd %xmm2, %xmm0, %xmm0
movdqu   0x10(%rsp), %xmm5          ; second accumulator: reload,
vpaddd   %xmm5, %xmm0, %xmm5
movdqu   %xmm5, 0x10(%rsp)          ; update, store, every iteration
testl    %edi, %edi
jne      <loop>

Only %xmm0–%xmm5 are used in the loop.

What happens

From the trace-log output (RUST_LOG=regalloc2=trace):

  1. Blockparam merging puts the second accumulator's in-loop vregs (v1081,
    v1767) into one bundle with the long-lived value they come from (v230 and
    its outer-loop block params v1150, v1184, v1130, …), about 30 vregs in
    all. That bundle (bundle15) spans the function's outer dispatch loop,
    including the blocks that make calls.
  2. Every vector register is clobbered across a call, so the bundle is split:
    split bundle LiveBundleIndex(15) at progpoint634-post, then at progpoint626-post.
  3. The third conflict hits the limit: Splitting bundle LiveBundleIndex(15) into minimal bundles. The inner loop's def and use each get a one-instruction
    bundle, and the range between them is in the spill bundle, which crosses the
    calls and ends up on the stack.

Confirming that it is the calls and the limit:

  • With the calls removed from the CLIF (replaced by constants), both
    accumulators stay in registers.
  • Sweeping MAX_SPLITS_PER_SPILLSET on this function:
limit inner loop (%rsp) references in the function
2 (current) load + store per iteration 171
3 load + store per iteration 174
4 register only 99
10 (the value #59 introduced) register only 99

Impact

On wasmtime with the inner loop's other bottleneck already removed
(bytecodealliance/wasmtime#14848), the kernel is 3–12% faster with the
spill gone, depending on shape:

int8 GEMV, m × k limit 2 limit 4
256 × 256 8.8 / 9.2 µs 8.6 / 8.5 µs
512 × 512 30.4 / 31.4 µs 29.0 / 27.7 µs
2048 × 512 121.1 / 128.1 µs 115.9 / 110.0 µs
512 × 2048 108.8 / 108.9 µs 99.5 / 95.4 µs

Two runs each, pinned to one core. A scalar kernel in the same module, which has
no such loop, is unchanged (12.8–13.4 µs at 256 × 256 in all four runs).

History

Possible fixes

  • Raise the limit. 4 is the smallest value that fixes this function; a PR with
    measurements follows.
  • Make the fallback less pessimistic: give the spill bundle's ranges inside loops
    free of clobbers a chance at a register, or count only splits that did not
    make progress. More involved; I have not tried it.

spill-in-loop.clif.txt

Activity

  1. cfallin commented on Oct 23, 2026

    @cfallin
    Member

    @EvanCarroll we had a similar issue in #265; there, though, the magic number was 3, not 4. Given the measurements in your companion PR, it seems we had a few nontrivial compile time regressions with 4.

    I suspect that @fitzgen might have some thoughts on better split heuristics here, as he has been working on similar things?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions