Skip to content

perf: fold LSB-test i32.and X 1 into i32.ctz in boolean contexts#8562

Open
ggreif wants to merge 3 commits intoWebAssembly:mainfrom
ggreif:gabor/lsb-if-ctz
Open

perf: fold LSB-test i32.and X 1 into i32.ctz in boolean contexts#8562
ggreif wants to merge 3 commits intoWebAssembly:mainfrom
ggreif:gabor/lsb-if-ctz

Conversation

@ggreif
Copy link
Copy Markdown

@ggreif ggreif commented Apr 1, 2026

Summary

An if-else conditioned on (i32.and X (i32.const 1)) tests the least significant bit of X. Since i32.ctz X == 0 iff the LSB of X is set, we can replace the condition with i32.ctz X and swap the branches — saving one instruction.

The second commit extends this to the primary pattern from the issue — eqz(and X 1) as a boolean condition (used in br_if, if, select) — handled in optimizeBoolean so all three sites benefit from one insertion.

  • Handles the constant on either side (left or right of and)
  • visitIf: (and X 1); if T E(ctz X); if E T
  • optimizeBoolean: eqz(and X 1)ctz X — covers the typical br_if (eqz (and X 1)) pattern

Motivation

Filed in #5752. The Motoko compiler already implements this in its own peephole optimizer (instrList.ml); the goal is to bring it to wasm-opt so that hand-written Wasm (e.g. the Motoko RTS, written in Rust) benefits too.

The optimizeBoolean rule alone fires 26–105 times across the three Motoko RTS variants (mo-rts-eop, mo-rts-incremental, mo-rts-non-incremental), targeting the is_skewed/is_scalar pointer-tagging checks in the GC hot path.

Applying wasm-opt --optimize-instructions to the Motoko RTS and running the benchmark suite shows the following gross effects (the submitted optimisation is a contributing factor alongside other rules triggered in the same pass):

Benchmark Before After Δ
heap-32 (GC-heavy, run 1) 1,153,792,735 instr 1,151,398,207 instr −2,394,528 (−0.21%)
heap-32 (run 2) 1,256,407,315 instr 1,253,408,059 instr −2,999,256 (−0.24%)
heap-64 (run 1) 1,324,057,357 instr 1,321,855,449 instr −2,201,908 (−0.17%)
heap-64 (run 2) 1,295,845,087 instr 1,293,744,743 instr −2,100,344 (−0.16%)
bignum 2,504,499 cycles 2,504,383 cycles −116
candid-subtype-cost 1,115,011 cycles 1,114,823 cycles −188

The GC-heavy heap benchmarks benefit most, consistent with the is_skewed check firing frequently during pointer traversal.

Test plan

  • New lit test test/lit/passes/optimize-instructions-lsb-if.wast covers if (const left and right) and br_if (eqz (and X 1))
  • All three test cases produce i32.ctz in the output

🤖 Generated with Claude Code

…X; if E T`

An if-else conditioned on `(i32.and X (i32.const 1))` tests the LSB of X.
Since `i32.ctz X == 0` iff the LSB of X is set, we can replace the condition
with `i32.ctz X` and swap the branches — saving one instruction.

Handles the constant on either side (left or right of `and`).

Relates to: WebAssembly#5752

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@ggreif ggreif requested a review from a team as a code owner April 1, 2026 09:39
@ggreif ggreif requested review from tlively and removed request for a team April 1, 2026 09:39
…an context

In boolean contexts (if, br_if, select), `eqz(and X 1)` and `ctz X` have
the same truthiness: both are truthy iff LSB(X) == 0. Replacing eqz+and
with ctz saves one instruction and covers the primary pattern from
WebAssembly#5752:

  i32.const 1; i32.and; i32.eqz; br_if N  ==>  i32.ctz; br_if N

This fires via `optimizeBoolean`, so it covers `if`, `br_if`, and `select`
conditions in one place. Observed ~26–105 hits across Motoko RTS variants.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@ggreif ggreif changed the title perf(OptimizeInstructions): fold i32.and X 1; if T E into i32.ctz X; if E T perf: fold LSB-test i32.and X 1 into i32.ctz in boolean contexts Apr 1, 2026
ggreif added a commit to caffeinelabs/motoko that referenced this pull request Apr 1, 2026
Add ggreif/binaryen (branch gabor/lsb-if-ctz-flake) as a flake input,
exposing a patched wasm-opt that folds LSB-test `i32.and X 1` patterns
into `i32.ctz` (WebAssembly/binaryen#8562). Apply it to the non-debug
RTS variants in installPhase, yielding ~0.2% instruction count reductions
in GC-heavy benchmarks.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@kripken
Copy link
Copy Markdown
Member

kripken commented Apr 1, 2026

Interesting. I worry this is not always faster, though: AND usually has a cost of 1, while TZCNT often has 2: https://www.agner.org/optimize/instruction_tables.pdf

Perhaps check what LLVM does here? They likely reasoned about this thoroughly.

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.

2 participants