From 51620019c84080b3a572ecdad7ebce6dabf76be8 Mon Sep 17 00:00:00 2001 From: artemkulyk Date: Sat, 26 Sep 2026 14:49:35 +0200 Subject: [PATCH] [LocalCSE] Consider size-2 expressions when shrinking Under -Os and -Oz, LocalCSE required an expression of IR size 3 before keeping a reused value in a local. A size-2 expression reused twice can still pay off: we drop one copy and add a local.set and a local.get. Allow size 2 in shrink modes. Skip it when the function has no locals at all, where coalesce-locals cannot merge the new local away and a single reuse would grow the output. Saves 22 KB on a 1650 module corpus, mostly in large functions that repeat small expressions such as (i32.load (local.get $p)). --- src/passes/LocalCSE.cpp | 43 +++++++++--- test/lit/passes/local-cse_shrink.wast | 66 +++++++++++++++++++ ...onversions-emscripten-modified.2asm.js.opt | 20 +++--- test/wasm2js/conversions-modified.2asm.js.opt | 20 +++--- 4 files changed, 121 insertions(+), 28 deletions(-) create mode 100644 test/lit/passes/local-cse_shrink.wast diff --git a/src/passes/LocalCSE.cpp b/src/passes/LocalCSE.cpp index 2ad15b731fe..cca1d4bdee2 100644 --- a/src/passes/LocalCSE.cpp +++ b/src/passes/LocalCSE.cpp @@ -358,12 +358,12 @@ struct Scanner return false; } - // If the size is at least 3, then if we have two of them we have 6, - // and so adding one set+one get and removing one of the items itself - // is not detrimental, and may be beneficial. - // TODO: investigate size 2 + // A size of 2 is the smallest where reuse can pay off: with two + // appearances we remove one of them while adding a local.set and a + // local.get. The Applier also skips the cases that are clearly not + // profitable, where the number of requests is known (see worthApplying). auto size = Measurer::measure(curr); - if (options.shrinkLevel > 0 && size >= 3) { + if (options.shrinkLevel > 0 && size >= 2) { return true; } @@ -586,13 +586,36 @@ struct Checker struct Applier : public LinearExecutionWalker> { RequestInfoMap requestInfos; + PassOptions& options; + + // Originals that we decided are not worth applying (in size-focused modes, + // where a small expression replaced a single time can end up larger, as we + // add a local set and get pair as well as a new local). + std::unordered_set skipped; - Applier(RequestInfoMap& requestInfos) : requestInfos(requestInfos) {} + Applier(RequestInfoMap& requestInfos, PassOptions& options) + : requestInfos(requestInfos), options(options) {} // Maps the original expressions that we save to locals to the local indexes // for them. std::unordered_map originalLocalMap; + bool worthApplying(Expression* curr, Index requests) { + if (options.shrinkLevel == 0) { + return true; + } + // A tiny expression (two IR nodes, e.g. a load of a local) replaced a + // single time is not worth it: we remove one small expression but add a + // local.set, a local.get, and a new local. If the function already has + // locals then coalesce-locals will usually merge the new local into one of + // them, so only restrict this when there are no locals at all. + if (Measurer::measure(curr) <= 2 && requests < 2 && + getFunction()->vars.empty()) { + return false; + } + return true; + } + void visitExpression(Expression* curr) { auto iter = requestInfos.find(curr); if (iter == requestInfos.end()) { @@ -603,6 +626,10 @@ struct Applier info.validate(); if (info.requests) { + if (!worthApplying(curr, info.requests)) { + skipped.insert(curr); + return; + } // We have requests for this value. Add a local and tee the value to // there. Index local = originalLocalMap[curr] = @@ -611,7 +638,7 @@ struct Applier Builder(*getModule()).makeLocalTee(local, curr, curr->type)); } else if (info.original) { auto& originalInfo = requestInfos.at(info.original); - if (originalInfo.requests) { + if (originalInfo.requests && !skipped.contains(info.original)) { // This is a valid request of an original value. Get the value from the // local. assert(originalLocalMap.contains(info.original)); @@ -663,7 +690,7 @@ struct LocalCSE : public WalkerPass> { return; } - Applier applier(requestInfos); + Applier applier(requestInfos, options); applier.walkFunctionInModule(func, getModule()); } }; diff --git a/test/lit/passes/local-cse_shrink.wast b/test/lit/passes/local-cse_shrink.wast new file mode 100644 index 00000000000..b85f188fa98 --- /dev/null +++ b/test/lit/passes/local-cse_shrink.wast @@ -0,0 +1,66 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. +;; RUN: foreach %s %t wasm-opt --local-cse --shrink-level=1 -S -o - | filecheck %s + +;; Under shrink levels, a size-2 expression reused once is only worth it when +;; the new local can be merged into an existing one. + +(module + (memory 1) + ;; CHECK: (type $0 (func (param i32) (result i32))) + + ;; CHECK: (memory $0 1) + + ;; CHECK: (export "no-locals" (func $no-locals)) + + ;; CHECK: (export "with-locals" (func $with-locals)) + + ;; CHECK: (func $no-locals (param $p i32) (result i32) + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (i32.load + ;; CHECK-NEXT: (local.get $p) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.load + ;; CHECK-NEXT: (local.get $p) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $no-locals (export "no-locals") (param $p i32) (result i32) + (i32.add + (i32.load + (local.get $p) + ) + (i32.load + (local.get $p) + ) + ) + ) + ;; CHECK: (func $with-locals (param $p i32) (result i32) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (local $2 i32) + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (local.tee $2 + ;; CHECK-NEXT: (i32.load + ;; CHECK-NEXT: (local.get $p) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $with-locals (export "with-locals") (param $p i32) (result i32) + (local $x i32) + (local.set $x + (i32.const 1) + ) + (i32.add + (i32.load + (local.get $p) + ) + (i32.load + (local.get $p) + ) + ) + ) +) diff --git a/test/wasm2js/conversions-emscripten-modified.2asm.js.opt b/test/wasm2js/conversions-emscripten-modified.2asm.js.opt index 860a022df46..0ee251443b0 100644 --- a/test/wasm2js/conversions-emscripten-modified.2asm.js.opt +++ b/test/wasm2js/conversions-emscripten-modified.2asm.js.opt @@ -37,28 +37,28 @@ function asmFunc(imports) { function legalstub$4($0_1) { var $1_1 = 0, $2_1 = 0; - $2_1 = ~~$0_1 >>> 0; + $1_1 = ~~$0_1 >>> 0; if (Math_fround(Math_abs($0_1)) >= Math_fround(1.0)) { - $1_1 = ~~($0_1 > Math_fround(0.0) ? Math_fround(Math_min(Math_fround(Math_floor(Math_fround($0_1 * Math_fround(2.3283064365386963e-10)))), Math_fround(4294967296.0))) : Math_fround(Math_ceil(Math_fround(Math_fround($0_1 - Math_fround(~~$0_1 >>> 0 >>> 0)) * Math_fround(2.3283064365386963e-10))))) >>> 0 + $2_1 = ~~($0_1 > Math_fround(0.0) ? Math_fround(Math_min(Math_fround(Math_floor(Math_fround($0_1 * Math_fround(2.3283064365386963e-10)))), Math_fround(4294967296.0))) : Math_fround(Math_ceil(Math_fround(Math_fround($0_1 - Math_fround($1_1 >>> 0)) * Math_fround(2.3283064365386963e-10))))) >>> 0 } else { - $1_1 = 0 + $2_1 = 0 } - i64toi32_i32$HIGH_BITS = $1_1; + i64toi32_i32$HIGH_BITS = $2_1; setTempRet0(i64toi32_i32$HIGH_BITS | 0); - return $2_1; + return $1_1; } function legalstub$6($0_1) { var $1_1 = 0, $2_1 = 0; - $2_1 = ~~$0_1 >>> 0; + $1_1 = ~~$0_1 >>> 0; if (Math_abs($0_1) >= 1.0) { - $1_1 = ~~($0_1 > 0.0 ? Math_min(Math_floor($0_1 * 2.3283064365386963e-10), 4294967295.0) : Math_ceil(($0_1 - +(~~$0_1 >>> 0 >>> 0)) * 2.3283064365386963e-10)) >>> 0 + $2_1 = ~~($0_1 > 0.0 ? Math_min(Math_floor($0_1 * 2.3283064365386963e-10), 4294967295.0) : Math_ceil(($0_1 - +($1_1 >>> 0)) * 2.3283064365386963e-10)) >>> 0 } else { - $1_1 = 0 + $2_1 = 0 } - i64toi32_i32$HIGH_BITS = $1_1; + i64toi32_i32$HIGH_BITS = $2_1; setTempRet0(i64toi32_i32$HIGH_BITS | 0); - return $2_1; + return $1_1; } // EMSCRIPTEN_END_FUNCS diff --git a/test/wasm2js/conversions-modified.2asm.js.opt b/test/wasm2js/conversions-modified.2asm.js.opt index 5eb7435ee84..8427b79f8a5 100644 --- a/test/wasm2js/conversions-modified.2asm.js.opt +++ b/test/wasm2js/conversions-modified.2asm.js.opt @@ -122,28 +122,28 @@ function asmFunc(imports) { function legalstub$7($0) { var $1 = 0, $2 = 0; - $2 = ~~$0 >>> 0; + $1 = ~~$0 >>> 0; if (Math_fround(Math_abs($0)) >= Math_fround(1.0)) { - $1 = ~~($0 > Math_fround(0.0) ? Math_fround(Math_min(Math_fround(Math_floor(Math_fround($0 * Math_fround(2.3283064365386963e-10)))), Math_fround(4294967296.0))) : Math_fround(Math_ceil(Math_fround(Math_fround($0 - Math_fround(~~$0 >>> 0 >>> 0)) * Math_fround(2.3283064365386963e-10))))) >>> 0 + $2 = ~~($0 > Math_fround(0.0) ? Math_fround(Math_min(Math_fround(Math_floor(Math_fround($0 * Math_fround(2.3283064365386963e-10)))), Math_fround(4294967296.0))) : Math_fround(Math_ceil(Math_fround(Math_fround($0 - Math_fround($1 >>> 0)) * Math_fround(2.3283064365386963e-10))))) >>> 0 } else { - $1 = 0 + $2 = 0 } - i64toi32_i32$HIGH_BITS = $1; + i64toi32_i32$HIGH_BITS = $2; setTempRet0(i64toi32_i32$HIGH_BITS | 0); - return $2; + return $1; } function legalstub$9($0) { var $1 = 0, $2 = 0; - $2 = ~~$0 >>> 0; + $1 = ~~$0 >>> 0; if (Math_abs($0) >= 1.0) { - $1 = ~~($0 > 0.0 ? Math_min(Math_floor($0 * 2.3283064365386963e-10), 4294967295.0) : Math_ceil(($0 - +(~~$0 >>> 0 >>> 0)) * 2.3283064365386963e-10)) >>> 0 + $2 = ~~($0 > 0.0 ? Math_min(Math_floor($0 * 2.3283064365386963e-10), 4294967295.0) : Math_ceil(($0 - +($1 >>> 0)) * 2.3283064365386963e-10)) >>> 0 } else { - $1 = 0 + $2 = 0 } - i64toi32_i32$HIGH_BITS = $1; + i64toi32_i32$HIGH_BITS = $2; setTempRet0(i64toi32_i32$HIGH_BITS | 0); - return $2; + return $1; } function legalstub$12($0, $1) {