diff options
-rw-r--r-- | src/ir/find_all.h | 2 | ||||
-rw-r--r-- | src/ir/properties.h | 5 | ||||
-rw-r--r-- | src/passes/OptimizeInstructions.cpp | 46 | ||||
-rw-r--r-- | test/lit/passes/optimize-instructions-gc-iit.wast | 18 | ||||
-rw-r--r-- | test/lit/passes/optimize-instructions-gc.wast | 185 |
5 files changed, 255 insertions, 1 deletions
diff --git a/src/ir/find_all.h b/src/ir/find_all.h index bd3d265e2..77edafe89 100644 --- a/src/ir/find_all.h +++ b/src/ir/find_all.h @@ -40,6 +40,8 @@ template<typename T> struct FindAll { finder.list = &list; finder.walk(ast); } + + bool has() { return !list.empty(); } }; // Find all pointers to instances of a certain node type diff --git a/src/ir/properties.h b/src/ir/properties.h index c4deac9d5..379a85708 100644 --- a/src/ir/properties.h +++ b/src/ir/properties.h @@ -223,6 +223,9 @@ inline Index getZeroExtBits(Expression* curr) { // and other operations that receive a value and let it flow through them. If // there is no value falling through, returns the node itself (as that is the // value that trivially falls through, with 0 steps in the middle). +// Note that this returns the value that would fall through if one does in fact +// do so. For example, the final element in a block may not fall through if we +// hit a return or a trap or an exception is thrown before we get there. inline Expression* getFallthrough(Expression* curr, const PassOptions& passOptions, FeatureSet features) { @@ -259,6 +262,8 @@ inline Expression* getFallthrough(Expression* curr, if (!EffectAnalyzer(passOptions, features, tryy->body).throws) { return getFallthrough(tryy->body, passOptions, features); } + } else if (auto* as = curr->dynCast<RefCast>()) { + return getFallthrough(as->ref, passOptions, features); } else if (auto* as = curr->dynCast<RefAs>()) { return getFallthrough(as->value, passOptions, features); } else if (auto* br = curr->dynCast<BrOn>()) { diff --git a/src/passes/OptimizeInstructions.cpp b/src/passes/OptimizeInstructions.cpp index 1f55c0a4b..3c29f7042 100644 --- a/src/passes/OptimizeInstructions.cpp +++ b/src/passes/OptimizeInstructions.cpp @@ -26,6 +26,7 @@ #include <ir/bits.h> #include <ir/cost.h> #include <ir/effects.h> +#include <ir/find_all.h> #include <ir/gc-type-utils.h> #include <ir/literal-utils.h> #include <ir/load-utils.h> @@ -992,6 +993,14 @@ struct OptimizeInstructions } } + void visitRefEq(RefEq* curr) { + // Identical references compare equal. + if (areConsecutiveInputsEqual(curr->left, curr->right)) { + replaceCurrent( + Builder(*getModule()).makeConst(Literal::makeOne(Type::i32))); + } + } + // If an instruction traps on a null input, there is no need for a // ref.as_non_null on that input: we will trap either way (and the binaryen // optimizer does not differentiate traps). @@ -1170,6 +1179,43 @@ private: // Information about our locals std::vector<LocalInfo> localInfo; + // Check if two consecutive inputs to an instruction are equal. As they are + // consecutive, no code can execeute in between them, which simplies the + // problem here (and which is the case we care about in this pass, which does + // simple peephole optimizations - all we care about is a single instruction + // at a time, and its inputs). + bool areConsecutiveInputsEqual(Expression* left, Expression* right) { + // First, check for side effects. If there are any, then we can't even + // assume things like local.get's of the same index being identical. + PassOptions passOptions = getPassOptions(); + if (EffectAnalyzer(passOptions, getModule()->features, left) + .hasSideEffects() || + EffectAnalyzer(passOptions, getModule()->features, right) + .hasSideEffects()) { + return false; + } + // Ignore extraneous things and compare them structurally. + left = Properties::getFallthrough(left, passOptions, getModule()->features); + right = + Properties::getFallthrough(right, passOptions, getModule()->features); + if (!ExpressionAnalyzer::equal(left, right)) { + return false; + } + // Reference equality also needs to check for allocation, which is *not* a + // side effect, but which results in different references: + // repeatedly calling (struct.new $foo)'s output will return different + // results (while i32.const etc. of course does not). + // Note that allocations may have appeared in the original inputs to this + // function, and skipped when we focused on what falls through; since there + // are no side effects, any allocations there cannot reach the fallthrough. + if (left->type.isRef()) { + if (FindAll<StructNew>(left).has() || FindAll<ArrayNew>(left).has()) { + return false; + } + } + return true; + } + // Canonicalizing the order of a symmetric binary helps us // write more concise pattern matching code elsewhere. void canonicalize(Binary* binary) { diff --git a/test/lit/passes/optimize-instructions-gc-iit.wast b/test/lit/passes/optimize-instructions-gc-iit.wast index 10bbbfc4a..5409e14c4 100644 --- a/test/lit/passes/optimize-instructions-gc-iit.wast +++ b/test/lit/passes/optimize-instructions-gc-iit.wast @@ -136,4 +136,22 @@ ) ) ) + + ;; CHECK: (func $ref-eq-ref-cast (param $x eqref) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $ref-eq-ref-cast (param $x eqref) + ;; we can look through a ref.cast if we ignore traps + (drop + (ref.eq + (local.get $x) + (ref.cast + (local.get $x) + (rtt.canon $parent) + ) + ) + ) + ) ) diff --git a/test/lit/passes/optimize-instructions-gc.wast b/test/lit/passes/optimize-instructions-gc.wast index b00d08193..33f7bef78 100644 --- a/test/lit/passes/optimize-instructions-gc.wast +++ b/test/lit/passes/optimize-instructions-gc.wast @@ -1,5 +1,5 @@ ;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. -;; RUN: wasm-opt %s --optimize-instructions --enable-reference-types --enable-gc -S -o - \ +;; RUN: wasm-opt %s --remove-unused-names --optimize-instructions --enable-reference-types --enable-gc -S -o - \ ;; RUN: | filecheck %s (module @@ -533,4 +533,187 @@ ) ) ) + + ;; CHECK: (func $get-eqref (result eqref) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + (func $get-eqref (result eqref) + (unreachable) + ) + + ;; CHECK: (func $ref-eq (param $x eqref) (param $y eqref) + ;; CHECK-NEXT: (local $lx eqref) + ;; CHECK-NEXT: (local $ly eqref) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.eq + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (local.get $y) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $lx + ;; CHECK-NEXT: (call $get-eqref) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $ref-eq (param $x eqref) (param $y eqref) + (local $lx eqref) + (local $ly eqref) + ;; identical parameters are equal + (drop + (ref.eq + (local.get $x) + (local.get $x) + ) + ) + ;; different ones might not be + (drop + (ref.eq + (local.get $x) + (local.get $y) + ) + ) + ;; identical locals are + (local.set $lx + (call $get-eqref) + ) + (drop + (ref.eq + (local.get $lx) + (local.get $lx) + ) + ) + ;; fallthroughs work ok (but we need --remove-unused-names so that we can + ;; trivially tell that there are no breaks) + (drop + (ref.eq + (block (result eqref) + (nop) + (local.get $x) + ) + (block (result eqref) + (nop) + (drop + (i32.const 10) + ) + (nop) + (local.get $x) + ) + ) + ) + ) + + (func $nothing) + + ;; CHECK: (func $ref-eq-corner-cases (param $x eqref) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.eq + ;; CHECK-NEXT: (block (result eqref) + ;; CHECK-NEXT: (call $nothing) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block (result eqref) + ;; CHECK-NEXT: (call $nothing) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.eq + ;; CHECK-NEXT: (struct.new_default_with_rtt $struct + ;; CHECK-NEXT: (rtt.canon $struct) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (struct.new_default_with_rtt $struct + ;; CHECK-NEXT: (rtt.canon $struct) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $ref-eq-corner-cases (param $x eqref) + ;; side effects prevent optimization + (drop + (ref.eq + (block (result eqref) + (call $nothing) + (local.get $x) + ) + (block (result eqref) + (call $nothing) + (local.get $x) + ) + ) + ) + ;; allocation prevents optimization + (drop + (ref.eq + (struct.new_default_with_rtt $struct + (rtt.canon $struct) + ) + (struct.new_default_with_rtt $struct + (rtt.canon $struct) + ) + ) + ) + ;; but irrelevant allocations do not prevent optimization + (drop + (ref.eq + (block (result eqref) + ;; an allocation that does not trouble us + (drop + (struct.new_default_with_rtt $struct + (rtt.canon $struct) + ) + ) + (local.get $x) + ) + (block (result eqref) + (drop + (struct.new_default_with_rtt $struct + (rtt.canon $struct) + ) + ) + ;; add a nop to make the two inputs to ref.eq not structurally equal, + ;; but in a way that does not matter (since only the value falling + ;; out does) + (nop) + (local.get $x) + ) + ) + ) + ) + + ;; CHECK: (func $ref-eq-ref-cast (param $x eqref) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.eq + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (ref.cast + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (rtt.canon $struct) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $ref-eq-ref-cast (param $x eqref) + ;; it is almost valid to look through a cast, except that it might trap so + ;; there is a side effect + (drop + (ref.eq + (local.get $x) + (ref.cast + (local.get $x) + (rtt.canon $struct) + ) + ) + ) + ) ) |