summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/ir/find_all.h2
-rw-r--r--src/ir/properties.h5
-rw-r--r--src/passes/OptimizeInstructions.cpp46
-rw-r--r--test/lit/passes/optimize-instructions-gc-iit.wast18
-rw-r--r--test/lit/passes/optimize-instructions-gc.wast185
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)
+ )
+ )
+ )
+ )
)