From 900f480438659aad2451cd85f16947d292a921df Mon Sep 17 00:00:00 2001 From: artemkulyk Date: Sat, 26 Sep 2026 14:54:33 +0200 Subject: [PATCH] [StackIR] Drop a set/get pair across a symmetric operation local2Stack gives up when a value sits between a local.set and the matching local.get. If that value and the get are consumed by the same operation whose operands can be swapped, the pair can be removed anyway. This also removes the one size regression of the late optimize-instructions round (4 bytes on a float test module), and is a small win on average. --- src/wasm-stack.h | 1 + src/wasm/wasm-stack-opts.cpp | 40 ++++++- test/lit/passes/optimize-stack-ir.wast | 140 ++++++++++++++++++++++--- 3 files changed, 165 insertions(+), 16 deletions(-) diff --git a/src/wasm-stack.h b/src/wasm-stack.h index fede0bf4ccf..c13595c2745 100644 --- a/src/wasm-stack.h +++ b/src/wasm-stack.h @@ -643,6 +643,7 @@ class StackIROptimizer { void removeAt(Index i); Index getNumConsumedValues(StackInst* inst); bool canRemoveSetGetPair(Index setIndex, Index getIndex); + bool isConsumedBySymmetricOp(Index getIndex); std::unordered_set findStringViewDeferredGets(); }; diff --git a/src/wasm/wasm-stack-opts.cpp b/src/wasm/wasm-stack-opts.cpp index 0050bd95fe5..3c1b8a69dce 100644 --- a/src/wasm/wasm-stack-opts.cpp +++ b/src/wasm/wasm-stack-opts.cpp @@ -21,6 +21,7 @@ #include "ir/branch-utils.h" #include "ir/iteration.h" #include "ir/local-graph.h" +#include "ir/properties.h" #include "pass.h" #include "wasm-stack.h" #include "wasm.h" @@ -124,6 +125,26 @@ void StackIROptimizer::vacuum() { } } +// Whether the local.get at getIndex is consumed by an operation whose operands +// can be swapped. +bool StackIROptimizer::isConsumedBySymmetricOp(Index getIndex) { + Index k = getIndex + 1; + while (k < insts.size() && !insts[k]) { + ++k; + } + if (k >= insts.size()) { + return false; + } + auto* inst = insts[k]; + if (inst->op != StackInst::Basic) { + return false; + } + if (auto* binary = inst->origin->dynCast()) { + return Properties::isSymmetric(binary); + } + return inst->origin->is(); +} + // If ordered properly, we can avoid a local.set/local.get pair, // and use the value directly from the stack, for example // [..produce a value on the stack..] @@ -219,11 +240,26 @@ void StackIROptimizer::local2Stack() { // can reach the set. if (values.size() > 0) { Index j = values.size() - 1; + // If an actual value is in the way then the set's value is not on + // top of the stack at the get. That is fine if exactly one value is + // in the way and it is consumed together with the get by an + // operation with interchangeable operands, as then the order does + // not matter. + bool intervening = false; while (1) { - // If there's an actual value in the way, we've failed. auto setIndex = values[j]; if (setIndex == null) { - break; + if (intervening || !isConsumedBySymmetricOp(getIndex)) { + // More than one value is in the way, or the value that is in + // the way is not consumed together with the get: give up. + break; + } + intervening = true; + if (j == 0) { + break; + } + j--; + continue; } auto* set = insts[setIndex]->origin->cast(); if (set->index == get->index) { diff --git a/test/lit/passes/optimize-stack-ir.wast b/test/lit/passes/optimize-stack-ir.wast index 123be3afb8a..5c3b591062f 100644 --- a/test/lit/passes/optimize-stack-ir.wast +++ b/test/lit/passes/optimize-stack-ir.wast @@ -6,16 +6,18 @@ ;; CHECK: (type $FUNCSIG$v (func)) (type $FUNCSIG$v (func)) - ;; CHECK: (type $5 (func (result i32))) - ;; CHECK: (type $6 (func (param i32) (result i32))) + ;; CHECK: (type $5 (func (result i32))) + ;; CHECK: (type $FUNCSIG$vf (func (param f32))) (type $FUNCSIG$vf (func (param f32))) ;; CHECK: (type $4 (func (result f64))) ;; CHECK: (type $FUNCSIG$ddd (func (param f64 f64) (result f64))) + ;; CHECK: (type $10 (func (param f32) (result f32))) + ;; CHECK: (type $FUNCSIG$id (func (param f64) (result i32))) (type $FUNCSIG$id (func (param f64) (result i32))) (type $FUNCSIG$ddd (func (param f64 f64) (result f64))) @@ -29,6 +31,10 @@ ;; CHECK: (type $9 (func (param i32 i64))) (type $9 (func (param i32 i64))) + ;; CHECK: (type $15 (func (param eqref) (result i32))) + + ;; CHECK: (type $16 (func (param eqref) (result eqref))) + ;; CHECK: (import "env" "_emscripten_asm_const_vi" (func $_emscripten_asm_const_vi (type $FUNCSIG$v))) (import "env" "_emscripten_asm_const_vi" (func $_emscripten_asm_const_vi)) @@ -1097,24 +1103,44 @@ ) ) - ;; CHECK: (func $local-to-stack-1c-no (type $6) (param $x i32) (result i32) + ;; CHECK: (func $local-to-stack-1c (type $6) (param $x i32) (result i32) ;; CHECK-NEXT: (local $temp i32) ;; CHECK-NEXT: i32.const 1 ;; CHECK-NEXT: call $local-to-stack - ;; CHECK-NEXT: local.set $temp ;; CHECK-NEXT: i32.const 2 ;; CHECK-NEXT: call $local-to-stack ;; CHECK-NEXT: drop ;; CHECK-NEXT: i32.const 3 - ;; CHECK-NEXT: local.get $temp ;; CHECK-NEXT: i32.add ;; CHECK-NEXT: ) - (func $local-to-stack-1c-no (param $x i32) (result i32) + (func $local-to-stack-1c (param $x i32) (result i32) (local $temp i32) (local.set $temp (call $local-to-stack (i32.const 1))) (drop (call $local-to-stack (i32.const 2))) (i32.add - (i32.const 3) ;; this is in the way + (i32.const 3) ;; in the way, but the add is commutative so we can + (local.get $temp) ;; still use the value on the stack + ) + ) + + ;; CHECK: (func $local-to-stack-1d-sub-no (type $6) (param $x i32) (result i32) + ;; CHECK-NEXT: (local $temp i32) + ;; CHECK-NEXT: i32.const 1 + ;; CHECK-NEXT: call $local-to-stack + ;; CHECK-NEXT: local.set $temp + ;; CHECK-NEXT: i32.const 2 + ;; CHECK-NEXT: call $local-to-stack + ;; CHECK-NEXT: drop + ;; CHECK-NEXT: i32.const 3 + ;; CHECK-NEXT: local.get $temp + ;; CHECK-NEXT: i32.sub + ;; CHECK-NEXT: ) + (func $local-to-stack-1d-sub-no (param $x i32) (result i32) + (local $temp i32) + (local.set $temp (call $local-to-stack (i32.const 1))) + (drop (call $local-to-stack (i32.const 2))) + (i32.sub + (i32.const 3) ;; not commutative: the set/get pair must stay (local.get $temp) ) ) @@ -1292,29 +1318,27 @@ (local.get $temp2) ) - ;; CHECK: (func $local-to-stack-overlapping-multi-8-no (type $6) (param $x i32) (result i32) + ;; CHECK: (func $local-to-stack-overlapping-multi-8 (type $6) (param $x i32) (result i32) ;; CHECK-NEXT: (local $temp1 i32) ;; CHECK-NEXT: (local $temp2 i32) ;; CHECK-NEXT: i32.const 1 ;; CHECK-NEXT: call $local-to-stack-multi-4 - ;; CHECK-NEXT: local.set $temp1 ;; CHECK-NEXT: i32.const 1 ;; CHECK-NEXT: call $local-to-stack-multi-4 ;; CHECK-NEXT: i32.const 3 ;; CHECK-NEXT: call $local-to-stack-multi-4 ;; CHECK-NEXT: drop - ;; CHECK-NEXT: local.get $temp1 ;; CHECK-NEXT: i32.add ;; CHECK-NEXT: ) - (func $local-to-stack-overlapping-multi-8-no (param $x i32) (result i32) + (func $local-to-stack-overlapping-multi-8 (param $x i32) (result i32) (local $temp1 i32) (local $temp2 i32) (local.set $temp1 (call $local-to-stack-multi-4 (i32.const 1))) (local.set $temp2 (call $local-to-stack-multi-4 (i32.const 1))) (drop (call $local-to-stack-multi-4 (i32.const 3))) - (i32.add - (local.get $temp2) ;; the timing - (local.get $temp1) ;; it sucks + (i32.add ;; the operands are flipped, but the add is commutative + (local.get $temp2) + (local.get $temp1) ) ) @@ -1459,4 +1483,92 @@ ) ) ) + + ;; A ref.eq with an intervening value: its operands can be swapped. + ;; CHECK: (func $local-to-stack-ref-eq (type $15) (param $x eqref) (result i32) + ;; CHECK-NEXT: (local $temp eqref) + ;; CHECK-NEXT: local.get $x + ;; CHECK-NEXT: call $eqref-value + ;; CHECK-NEXT: local.get $x + ;; CHECK-NEXT: call $eqref-value + ;; CHECK-NEXT: drop + ;; CHECK-NEXT: local.get $x + ;; CHECK-NEXT: ref.eq + ;; CHECK-NEXT: ) + (func $local-to-stack-ref-eq (param $x eqref) (result i32) + (local $temp eqref) + (local.set $temp (call $eqref-value (local.get $x))) + (drop (call $eqref-value (local.get $x))) + (ref.eq + (local.get $x) + (local.get $temp) + ) + ) + + ;; CHECK: (func $eqref-value (type $16) (param $x eqref) (result eqref) + ;; CHECK-NEXT: local.get $x + ;; CHECK-NEXT: call $eqref-value + ;; CHECK-NEXT: ) + (func $eqref-value (param $x eqref) (result eqref) + (call $eqref-value (local.get $x)) + ) + + ;; f32.min has swappable operands, so the set/get pair can be removed. + ;; CHECK: (func $local-to-stack-f32-min (type $10) (param $x f32) (result f32) + ;; CHECK-NEXT: (local $temp f32) + ;; CHECK-NEXT: local.get $x + ;; CHECK-NEXT: call $float-value + ;; CHECK-NEXT: local.get $x + ;; CHECK-NEXT: call $float-value + ;; CHECK-NEXT: drop + ;; CHECK-NEXT: local.get $x + ;; CHECK-NEXT: f32.min + ;; CHECK-NEXT: ) + (func $local-to-stack-f32-min (param $x f32) (result f32) + (local $temp f32) + (local.set $temp (call $float-value (local.get $x))) + (drop (call $float-value (local.get $x))) + (f32.min + (local.get $x) + (local.get $temp) + ) + ) + + ;; CHECK: (func $float-value (type $10) (param $x f32) (result f32) + ;; CHECK-NEXT: local.get $x + ;; CHECK-NEXT: call $float-value + ;; CHECK-NEXT: ) + (func $float-value (param $x f32) (result f32) + (call $float-value (local.get $x)) + ) + + ;; Two values are in the way, so the set/get pair must stay. + ;; CHECK: (func $local-to-stack-two-in-the-way (type $6) (param $x i32) (result i32) + ;; CHECK-NEXT: (local $temp i32) + ;; CHECK-NEXT: i32.const 1 + ;; CHECK-NEXT: call $local-to-stack + ;; CHECK-NEXT: local.set $temp + ;; CHECK-NEXT: i32.const 2 + ;; CHECK-NEXT: call $local-to-stack + ;; CHECK-NEXT: drop + ;; CHECK-NEXT: i32.const 3 + ;; CHECK-NEXT: call $local-to-stack + ;; CHECK-NEXT: i32.const 4 + ;; CHECK-NEXT: call $local-to-stack + ;; CHECK-NEXT: local.get $temp + ;; CHECK-NEXT: i32.add + ;; CHECK-NEXT: i32.add + ;; CHECK-NEXT: ) + (func $local-to-stack-two-in-the-way (param $x i32) (result i32) + (local $temp i32) + (local.set $temp (call $local-to-stack (i32.const 1))) + (drop (call $local-to-stack (i32.const 2))) + (i32.add + (call $local-to-stack (i32.const 3)) + (i32.add + (call $local-to-stack (i32.const 4)) + (local.get $temp) + ) + ) + ) )