Skip to content

Expose loop-invariant inputs to GVN and LICM - #345

Merged
jserv merged 2 commits into
masterfrom
vir-trivial-params
Oct 15, 2026
Merged

jserv merged 2 commits into
masterfrom
vir-trivial-params

Conversation

@jserv

@jserv jserv commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator

Loop-invariant inputs reached loop headers as block parameters that the back edge passes to itself. SSA sealing seals each block once in layout order, so a header is sealed while its back edge still carries the loop body's unsealed parameters, and nothing revisits it once those resolve to the header's own parameter. The only pass that removed what was left ran inside strength reduction, after GVN and LICM, so LICM hoisted nothing from for (i = 0; i < n; i++) a[i] = i + x * y; and GVN could not match the in-loop x * y with the same product computed before the loop. The first commit makes parameter simplification its own pass after the first CFG simplification. It leaves the entry block's parameters (the function's arguments) alone, and builds dominance only when some parameter has a single incoming value. The second commit folds a branch whose two arms reach the same block with the same arguments into a jump; x86-64 used to emit test edi, edi; jne +0 for if (c) y = x; else y = x;.

Verification: make bootstrap (stage 1 and stage 2 byte-identical) and make check pass on x64, arm, riscv and arm64 for the final tree, with bootstrap on all four plus x64 make check for the first commit alone. make check-vir passes on x64, tests/vir-core.c passes under ASan and UBSan, and .ci/check-commentflow.sh, .ci/check-format.sh and .ci/check-newline.sh are clean. New unit tests cover parameter simplification feeding GVN and LICM, entry parameters surviving a back edge into the entry block, and the fold with equal and with different arguments; each fails when its fix is reverted.

Measurements on a Ryzen Threadripper 2990WX, in user-space instructions (perf stat -e instructions:u; under QEMU this counts QEMU's host instructions). For a loop using x * y, y ^ z and x << 3:

Target Before After Change
x86-64 (native) 300.8M 221.0M -26.5%
Armv7 (QEMU) 1442.2M 1322.5M -8.3%
AArch64 (QEMU) 1190.1M 1050.3M -11.7%
RV32 (QEMU) 3038.4M 801.1M -73.6%

The seven native throughput fixtures compile to the same bytes. A stage 0 self-compile runs about 1% more instructions for the extra pass, and the stage 1 compiler it builds runs 0.2% fewer.

Closes #283
Closes #290


Summary by cubic

Exposes loop-invariant inputs to GVN and LICM by simplifying block parameters before those passes run. A loop-invariant value reached a loop header as a block parameter that the back edge passed to itself, with the only cleanup running inside strength reduction, after GVN and LICM, so LICM hoisted nothing from for (i = 0; i < n; i++) a[i] = i + x * y; and GVN could not match the in-loop x * y with the same product computed before the loop.

Adds vir_simplify_params as its own pass after the first CFG simplification. It replaces each parameter receiving one value on every edge (or itself on a back edge) with that value, and leaves the entry block's parameters alone. Also folds a branch whose two arms reach the same block with the same arguments into a jump; x86-64 used to emit test edi, edi; jne +0 for if (c) y = x; else y = x;.

A loop using x * y, y ^ z, and x << 3 runs 26.5% fewer user-space instructions natively on x86-64; under QEMU it drops 8.3% on Armv7, 11.7% on AArch64, and 73.6% on RV32. make bootstrap produces byte-identical stage 1 and stage 2, and make check passes on x86-64, Arm, RISC-V, and arm64. New unit tests cover parameter simplification feeding GVN and LICM, an entry parameter surviving a back edge, and the branch fold with equal and with different arguments.

Written for commit 1742111. Summary will update on new commits.

View guided diff Turn on auto-fix

jserv added 2 commits October 15, 2026 22:49
A value that enters a loop and is never reassigned there reached the
loop header as a block parameter that the back edge passes to itself.
SSA sealing removes such parameters, but it seals each block once in
layout order: the header is sealed while its back edge still carries
the loop body's own, not yet sealed, parameters, and only once the body
is sealed do those turn out to be the header's parameter. Nothing looks
at the header again. The one pass that dropped what was left ran inside
strength reduction, after GVN and LICM had treated every use of the
parameter as defined inside the loop, so LICM hoisted nothing from

    for (i = 0; i < n; i++)
        a[i] = i + x * y;

and GVN could not match x * y in the loop with the same product
computed before it.

Parameter simplification is now a pass of its own, run after the first
CFG simplification so that local CSE, GVN and LICM see the input
itself. It leaves the entry block's parameters alone: they are the
function's arguments, and a back edge into the entry block that passes
one value does not make an argument redundant. Dominance is built only
once some parameter has a single incoming value.

A loop using x * y, y ^ z and x << 3 runs 26.5 percent fewer
instructions on x86-64. Under QEMU its host instructions drop by 8
percent for Armv7, 12 percent for AArch64 and 74 percent for RV32. The
native throughput fixtures compile to the same bytes. A stage 0
self-compile runs about 1 percent more instructions, and the stage 1
compiler it builds runs 0.2 percent fewer.

Close #283
When CFG simplification bypasses the empty arms of a diamond such as

    if (c)
        y = x;
    else
        y = x;

both edges of the branch end up on the join with the same arguments.
The branch was kept, so its condition stayed live and x86-64 emitted
"test edi, edi; jne +0" in front of the join.

CFG simplification now turns such a branch into a jump, and DCE drops
the condition. Edges that pass different arguments still select a
value and stay a branch. Folding only deletes an edge, so the scan
continues in place instead of restarting from the first block.

Close #290
cubic-dev-ai[bot]

This comment was marked as resolved.

@jserv
jserv merged commit ba37733 into master Oct 15, 2026
55 checks passed
@jserv
jserv deleted the vir-trivial-params branch October 15, 2026 22:55
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.

Optimize branch patterns in peephole optimizer Implement Loop-Invariant Code Motion (LICM) optimization

1 participant