Expose loop-invariant inputs to GVN and LICM

Merged
JS
15d ago
+257-30

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:

TargetBeforeAfterChange
x86-64 (native)300.8M221.0M-26.5%
Armv7 (QEMU)1442.2M1322.5M-8.3%
AArch64 (QEMU)1190.1M1050.3M-11.7%
RV32 (QEMU)3038.4M801.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.