From 9b55439e7b1fca2d17c268613a3e902d42a25c72 Mon Sep 17 00:00:00 2001 From: Gabor Greif Date: Wed, 1 Apr 2026 11:38:43 +0200 Subject: [PATCH 1/3] perf(OptimizeInstructions): fold `i32.and X 1; if T E` into `i32.ctz X; if E T` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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: https://github.com/WebAssembly/binaryen/issues/5752 Co-Authored-By: Claude Sonnet 4.6 --- flake.lock | 61 +++++++++++++++++++ flake.nix | 26 ++++++++ src/passes/OptimizeInstructions.cpp | 22 +++++++ .../passes/optimize-instructions-lsb-if.wast | 52 ++++++++++++++++ 4 files changed, 161 insertions(+) create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 test/lit/passes/optimize-instructions-lsb-if.wast diff --git a/flake.lock b/flake.lock new file mode 100644 index 00000000000..db686c56b7d --- /dev/null +++ b/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1767313136, + "narHash": "sha256-16KkgfdYqjaeRGBaYsNrhPRRENs0qzkQVUooNHtoy2w=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "ac62194c3917d5f474c1a844b6fd6da2db95077d", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-25.05", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 00000000000..9ac42e10601 --- /dev/null +++ b/flake.nix @@ -0,0 +1,26 @@ +{ + description = "Binaryen development shell"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils }: + flake-utils.lib.eachDefaultSystem (system: + let pkgs = nixpkgs.legacyPackages.${system}; in + { + devShells.default = pkgs.mkShell { + buildInputs = with pkgs; [ + cmake + ninja + clang_19 + python3 + ]; + shellHook = '' + export CC=${pkgs.clang_19}/bin/clang + export CXX=${pkgs.clang_19}/bin/clang++ + ''; + }; + }); +} diff --git a/src/passes/OptimizeInstructions.cpp b/src/passes/OptimizeInstructions.cpp index e388775caab..bb351c97f43 100644 --- a/src/passes/OptimizeInstructions.cpp +++ b/src/passes/OptimizeInstructions.cpp @@ -1193,6 +1193,28 @@ struct OptimizeInstructions BranchHints::flip(curr, getFunction()); } } + // (i32.and X 1) as if-else condition => (i32.ctz X) with swapped arms, + // since ctz(X) == 0 iff LSB(X) == 1 (saves one instruction). + if (auto* binary = curr->condition->dynCast()) { + if (binary->op == AndInt32) { + Expression* other = nullptr; + if (auto* c = binary->right->dynCast()) { + if (c->value.geti32() == 1) { + other = binary->left; + } + } else if (auto* c = binary->left->dynCast()) { + if (c->value.geti32() == 1) { + other = binary->right; + } + } + if (other) { + Builder builder(*getModule()); + curr->condition = builder.makeUnary(CtzInt32, other); + std::swap(curr->ifTrue, curr->ifFalse); + BranchHints::flip(curr, getFunction()); + } + } + } // Note that we do not consider metadata here. Like LLVM, we ignore // metadata when trying to fold code together, preferring certain // optimization over possible benefits of profiling data. diff --git a/test/lit/passes/optimize-instructions-lsb-if.wast b/test/lit/passes/optimize-instructions-lsb-if.wast new file mode 100644 index 00000000000..48580adc071 --- /dev/null +++ b/test/lit/passes/optimize-instructions-lsb-if.wast @@ -0,0 +1,52 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. +;; RUN: wasm-opt %s --optimize-instructions -S -o - | filecheck %s + +;; Test that (if (i32.and X (i32.const 1)) T E) is optimized to +;; (if (i32.ctz X) E T), saving one instruction. + +(module + ;; CHECK: (func $lsb-if (param $x i32) (result i32) + ;; CHECK-NEXT: (if (result i32) + ;; CHECK-NEXT: (i32.ctz + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (else + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $lsb-if (param $x i32) (result i32) + ;; if LSB is set, return 1; else return 0 + ;; optimizes to: if ctz(x) != 0, return 0; else return 1 + (if (result i32) + (i32.and (local.get $x) (i32.const 1)) + (then (i32.const 1)) + (else (i32.const 0)) + ) + ) + + ;; CHECK: (func $lsb-if-const-left (param $x i32) (result i32) + ;; CHECK-NEXT: (if (result i32) + ;; CHECK-NEXT: (i32.ctz + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (else + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $lsb-if-const-left (param $x i32) (result i32) + ;; same but constant on the left + (if (result i32) + (i32.and (i32.const 1) (local.get $x)) + (then (i32.const 1)) + (else (i32.const 0)) + ) + ) +) From bff1a50235b652ae1ac2e3b669760e70a2ac3c1e Mon Sep 17 00:00:00 2001 From: Gabor Greif Date: Wed, 1 Apr 2026 12:08:08 +0200 Subject: [PATCH 2/3] perf(OptimizeInstructions): fold `eqz(and X 1)` into `ctz X` in boolean context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 https://github.com/WebAssembly/binaryen/issues/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 --- src/passes/OptimizeInstructions.cpp | 18 ++++++++++++ .../passes/optimize-instructions-lsb-if.wast | 29 ++++++++++++++++++- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/passes/OptimizeInstructions.cpp b/src/passes/OptimizeInstructions.cpp index bb351c97f43..1574fd0a160 100644 --- a/src/passes/OptimizeInstructions.cpp +++ b/src/passes/OptimizeInstructions.cpp @@ -3136,6 +3136,24 @@ struct OptimizeInstructions binary->op = op; return binary; } + // eqz(and X 1) ==> ctz X in boolean context: + // both are truthy iff LSB(X) == 0, saving one instruction. + if (binary->op == AndInt32) { + Expression* other = nullptr; + if (auto* c = binary->right->dynCast()) { + if (c->value.geti32() == 1) { + other = binary->left; + } + } else if (auto* c = binary->left->dynCast()) { + if (c->value.geti32() == 1) { + other = binary->right; + } + } + if (other) { + Builder builder(*getModule()); + return builder.makeUnary(CtzInt32, other); + } + } } } if (unary->op == EqZInt32 || unary->op == EqZInt64) { diff --git a/test/lit/passes/optimize-instructions-lsb-if.wast b/test/lit/passes/optimize-instructions-lsb-if.wast index 48580adc071..fbd6eca3179 100644 --- a/test/lit/passes/optimize-instructions-lsb-if.wast +++ b/test/lit/passes/optimize-instructions-lsb-if.wast @@ -2,7 +2,8 @@ ;; RUN: wasm-opt %s --optimize-instructions -S -o - | filecheck %s ;; Test that (if (i32.and X (i32.const 1)) T E) is optimized to -;; (if (i32.ctz X) E T), saving one instruction. +;; (if (i32.ctz X) E T), and (br_if N V (i32.eqz (i32.and X 1))) to +;; (br_if N V (i32.ctz X)), saving one instruction in each case. (module ;; CHECK: (func $lsb-if (param $x i32) (result i32) @@ -49,4 +50,30 @@ (else (i32.const 0)) ) ) + + ;; CHECK: (func $lsb-brif (param $x i32) (result i32) + ;; CHECK-NEXT: (block $done (result i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (br_if $done + ;; CHECK-NEXT: (i32.const 99) + ;; CHECK-NEXT: (i32.ctz + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $lsb-brif (param $x i32) (result i32) + ;; br_if (eqz (and X 1)) => br_if (ctz X): the typical is_skewed/is_scalar pattern + (block $done (result i32) + (drop + (br_if $done + (i32.const 99) + (i32.eqz (i32.and (local.get $x) (i32.const 1))) + ) + ) + (i32.const 42) + ) + ) ) From 575cd27b58ddec43a24636574a45b6411210881a Mon Sep 17 00:00:00 2001 From: Gabor Greif Date: Wed, 1 Apr 2026 12:56:41 +0200 Subject: [PATCH 3/3] chore: remove nix files (not for upstream) --- flake.lock | 61 ------------------------------------------------------ flake.nix | 26 ----------------------- 2 files changed, 87 deletions(-) delete mode 100644 flake.lock delete mode 100644 flake.nix diff --git a/flake.lock b/flake.lock deleted file mode 100644 index db686c56b7d..00000000000 --- a/flake.lock +++ /dev/null @@ -1,61 +0,0 @@ -{ - "nodes": { - "flake-utils": { - "inputs": { - "systems": "systems" - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "nixpkgs": { - "locked": { - "lastModified": 1767313136, - "narHash": "sha256-16KkgfdYqjaeRGBaYsNrhPRRENs0qzkQVUooNHtoy2w=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "ac62194c3917d5f474c1a844b6fd6da2db95077d", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixos-25.05", - "repo": "nixpkgs", - "type": "github" - } - }, - "root": { - "inputs": { - "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs" - } - }, - "systems": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - } - }, - "root": "root", - "version": 7 -} diff --git a/flake.nix b/flake.nix deleted file mode 100644 index 9ac42e10601..00000000000 --- a/flake.nix +++ /dev/null @@ -1,26 +0,0 @@ -{ - description = "Binaryen development shell"; - - inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05"; - flake-utils.url = "github:numtide/flake-utils"; - }; - - outputs = { self, nixpkgs, flake-utils }: - flake-utils.lib.eachDefaultSystem (system: - let pkgs = nixpkgs.legacyPackages.${system}; in - { - devShells.default = pkgs.mkShell { - buildInputs = with pkgs; [ - cmake - ninja - clang_19 - python3 - ]; - shellHook = '' - export CC=${pkgs.clang_19}/bin/clang - export CXX=${pkgs.clang_19}/bin/clang++ - ''; - }; - }); -}