summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/ir/effects.h33
-rw-r--r--src/pass.h30
-rw-r--r--src/passes/OptimizeInstructions.cpp26
-rw-r--r--src/passes/Vacuum.cpp21
-rw-r--r--src/tools/optimization-options.h8
-rw-r--r--test/lit/help/optimization-opts.test5
-rw-r--r--test/lit/passes/optimize-instructions-gc-iit.wast70
-rw-r--r--test/lit/passes/vacuum-tnh.wast74
8 files changed, 254 insertions, 13 deletions
diff --git a/src/ir/effects.h b/src/ir/effects.h
index 2de5c54f6..f5bb0e6cb 100644
--- a/src/ir/effects.h
+++ b/src/ir/effects.h
@@ -30,6 +30,7 @@ public:
FeatureSet features,
Expression* ast = nullptr)
: ignoreImplicitTraps(passOptions.ignoreImplicitTraps),
+ trapsNeverHappen(passOptions.trapsNeverHappen),
debugInfo(passOptions.debugInfo), features(features) {
if (ast) {
walk(ast);
@@ -37,6 +38,7 @@ public:
}
bool ignoreImplicitTraps;
+ bool trapsNeverHappen;
bool debugInfo;
FeatureSet features;
@@ -143,10 +145,37 @@ public:
return globalsRead.size() || readsMemory || readsHeap || isAtomic || calls;
}
- bool hasSideEffects() const {
+ bool hasNonTrapSideEffects() const {
return localsWritten.size() > 0 || danglingPop || writesGlobalState() ||
- trap || throws || transfersControlFlow();
+ throws || transfersControlFlow();
}
+
+ bool hasSideEffects() const { return trap || hasNonTrapSideEffects(); }
+
+ // Check if there are side effects, and they are of a kind that cannot be
+ // removed by optimization passes.
+ //
+ // The difference between this and hasSideEffects is subtle, and only related
+ // to trapsNeverHappen - if trapsNeverHappen then any trap we see is removable
+ // by optimizations. In general, you should call hasSideEffects, and only call
+ // this method if you are certain that it is a place that would not perform an
+ // unsafe transformation with a trap. Specifically, if a pass calls this
+ // and gets the result that there are no unremovable side effects, then it
+ // must either
+ //
+ // 1. Remove any side effects present, if any, so they no longer exists.
+ // 2. Keep the code exactly where it is.
+ //
+ // If instead of 1&2 a pass kept the side effect and also reordered the code
+ // with other things, then that could be bad, as the side effect might have
+ // been behind a condition that avoids it occurring.
+ //
+ // TODO: Go through the optimizer and use this in all places that do not move
+ // code around.
+ bool hasUnremovableSideEffects() const {
+ return hasNonTrapSideEffects() || (trap && !trapsNeverHappen);
+ }
+
bool hasAnything() const {
return hasSideEffects() || accessesLocal() || readsMemory ||
accessesGlobal();
diff --git a/src/pass.h b/src/pass.h
index 0be45e2d1..08237b271 100644
--- a/src/pass.h
+++ b/src/pass.h
@@ -99,6 +99,36 @@ struct PassOptions {
InliningOptions inlining;
// Optimize assuming things like div by 0, bad load/store, will not trap.
bool ignoreImplicitTraps = false;
+ // Optimize assuming a trap will never happen at runtime. This is similar to
+ // ignoreImplicitTraps, but different:
+ //
+ // * ignoreImplicitTraps simply ignores the side effect of trapping when it
+ // computes side effects, and then passes work with that data.
+ // * trapsNeverHappen assumes that if an instruction with a possible trap is
+ // reached, then it does not trap, and an (unreachable) - that always
+ // traps - is never reached.
+ //
+ // The main difference is that in trapsNeverHappen mode we will not move
+ // around code that might trap, like this:
+ //
+ // (if (condition) (code))
+ //
+ // If (code) might trap, ignoreImplicitTraps ignores that trap, and it might
+ // end up moving (code) to happen before the (condition), that is,
+ // unconditionally. trapsNeverHappen, on the other hand, does not ignore the
+ // side effect of the trap; instead, it will potentially remove the trapping
+ // instruction, if it can - it is always safe to remove a trap in this mode,
+ // as the traps are assumed to not happen. Where it cannot remove the side
+ // effect, it will at least not move code around.
+ //
+ // A consequence of this difference is that code that puts a possible trap
+ // behind a condition is unsafe in ignoreImplicitTraps, but safe in
+ // trapsNeverHappen. In general, trapsNeverHappen is safe on production code
+ // where traps are either fatal errors or assertions, and it is assumed
+ // neither of those can happen (and it is undefined behavior if they do).
+ //
+ // TODO: deprecate and remove ignoreImplicitTraps.
+ bool trapsNeverHappen = false;
// Optimize assuming that the low 1K of memory is not valid memory for the
// application to use. In that case, we can optimize load/store offsets in
// many cases.
diff --git a/src/passes/OptimizeInstructions.cpp b/src/passes/OptimizeInstructions.cpp
index 43d0d912f..de7400ae0 100644
--- a/src/passes/OptimizeInstructions.cpp
+++ b/src/passes/OptimizeInstructions.cpp
@@ -1201,7 +1201,7 @@ struct OptimizeInstructions
void visitRefEq(RefEq* curr) {
// Identical references compare equal.
- if (areConsecutiveInputsEqual(curr->left, curr->right)) {
+ if (areConsecutiveInputsEqualAndRemovable(curr->left, curr->right)) {
replaceCurrent(
Builder(*getModule()).makeConst(Literal::makeOne(Type::i32)));
return;
@@ -1270,7 +1270,7 @@ struct OptimizeInstructions
auto passOptions = getPassOptions();
- if (passOptions.ignoreImplicitTraps) {
+ if (passOptions.ignoreImplicitTraps || passOptions.trapsNeverHappen) {
// A ref.cast traps when the RTTs do not line up, which can be of one of
// two sorts of issues:
// 1. The value being cast is not even a subtype of the cast type. In
@@ -1281,7 +1281,7 @@ struct OptimizeInstructions
// fit. That indicates a difference between RTT subtyping and static
// subtyping. That is, the type may be right but the chain of rtt.subs
// is not.
- // If we ignore implicit traps then we would like to assume that neither
+ // If we ignore a possible trap then we would like to assume that neither
// of those two situations can happen. However, we still cannot do
// anything if point 1 is a problem, that is, if the value is not a
// subtype of the cast type, as we can't remove the cast in that case -
@@ -1466,15 +1466,23 @@ private:
// 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) {
+ //
+ // This also checks that the inputs are removable.
+ bool areConsecutiveInputsEqualAndRemovable(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.
+ // assume things like local.get's of the same index being identical. (It is
+ // also ok to have side effects here, if we can remove them, as we are also
+ // checking if we can remove the two inputs anyhow.)
auto passOptions = getPassOptions();
auto features = getModule()->features;
- if (EffectAnalyzer(passOptions, features, left).hasSideEffects() ||
- EffectAnalyzer(passOptions, features, right).hasSideEffects()) {
+ if (EffectAnalyzer(passOptions, features, left)
+ .hasUnremovableSideEffects() ||
+ EffectAnalyzer(passOptions, features, right)
+ .hasUnremovableSideEffects()) {
return false;
}
+
// Ignore extraneous things and compare them structurally.
left = Properties::getFallthrough(left, passOptions, features);
right = Properties::getFallthrough(right, passOptions, features);
@@ -2717,7 +2725,7 @@ private:
Expression* optimizeMemoryCopy(MemoryCopy* memCopy) {
PassOptions options = getPassOptions();
- if (options.ignoreImplicitTraps) {
+ if (options.ignoreImplicitTraps || options.trapsNeverHappen) {
if (ExpressionAnalyzer::equal(memCopy->dest, memCopy->source)) {
// memory.copy(x, x, sz) ==> {drop(x), drop(x), drop(sz)}
Builder builder(*getModule());
@@ -2734,7 +2742,7 @@ private:
switch (bytes) {
case 0: {
- if (options.ignoreImplicitTraps) {
+ if (options.ignoreImplicitTraps || options.trapsNeverHappen) {
// memory.copy(dst, src, 0) ==> {drop(dst), drop(src)}
return builder.makeBlock({builder.makeDrop(memCopy->dest),
builder.makeDrop(memCopy->source)});
diff --git a/src/passes/Vacuum.cpp b/src/passes/Vacuum.cpp
index 25b0116b3..14a464297 100644
--- a/src/passes/Vacuum.cpp
+++ b/src/passes/Vacuum.cpp
@@ -253,9 +253,8 @@ struct Vacuum : public WalkerPass<ExpressionStackWalker<Vacuum>> {
}
}
} else {
- // no else
+ // This is an if without an else. If the body is empty, we do not need it.
if (curr->ifTrue->is<Nop>()) {
- // no nothing
replaceCurrent(Builder(*getModule()).makeDrop(curr->condition));
}
}
@@ -281,6 +280,24 @@ struct Vacuum : public WalkerPass<ExpressionStackWalker<Vacuum>> {
replaceCurrent(set);
return;
}
+
+ // If the value has no side effects, or it has side effects we can remove,
+ // do so. This basically means that if noTrapsHappen is set then we can
+ // use that assumption (that no trap actually happens at runtime) and remove
+ // a trapping value.
+ //
+ // TODO: A complete CFG analysis for noTrapsHappen mode, removing all code
+ // that definitely reaches a trap, *even if* it has side effects.
+ //
+ // Note that we check the type here to avoid removing unreachable code - we
+ // leave that for DCE.
+ if (curr->type == Type::none &&
+ !EffectAnalyzer(getPassOptions(), getModule()->features, curr)
+ .hasUnremovableSideEffects()) {
+ ExpressionManipulator::nop(curr);
+ return;
+ }
+
// if we are dropping a block's return value, we might be able to remove it
// entirely
if (auto* block = curr->value->dynCast<Block>()) {
diff --git a/src/tools/optimization-options.h b/src/tools/optimization-options.h
index b7db5d531..58a8e3980 100644
--- a/src/tools/optimization-options.h
+++ b/src/tools/optimization-options.h
@@ -179,6 +179,14 @@ struct OptimizationOptions : public ToolOptions {
[this](Options*, const std::string&) {
passOptions.ignoreImplicitTraps = true;
})
+ .add("--traps-never-happen",
+ "-tnh",
+ "Optimize under the helpful assumption that no trap is reached at "
+ "runtime (from load, div/mod, etc.)",
+ Options::Arguments::Zero,
+ [this](Options*, const std::string&) {
+ passOptions.trapsNeverHappen = true;
+ })
.add("--low-memory-unused",
"-lmu",
"Optimize under the helpful assumption that the low 1K of memory is "
diff --git a/test/lit/help/optimization-opts.test b/test/lit/help/optimization-opts.test
index d189c6398..52695077a 100644
--- a/test/lit/help/optimization-opts.test
+++ b/test/lit/help/optimization-opts.test
@@ -161,6 +161,11 @@
;; CHECK-NEXT: traps occur (from load, div/mod,
;; CHECK-NEXT: etc.)
;; CHECK-NEXT:
+;; CHECK-NEXT: --traps-never-happen,-tnh Optimize under the helpful
+;; CHECK-NEXT: assumption that no trap is
+;; CHECK-NEXT: reached at runtime (from load,
+;; CHECK-NEXT: div/mod, etc.)
+;; CHECK-NEXT:
;; CHECK-NEXT: --low-memory-unused,-lmu Optimize under the helpful
;; CHECK-NEXT: assumption that the low 1K of
;; CHECK-NEXT: memory is not used by the
diff --git a/test/lit/passes/optimize-instructions-gc-iit.wast b/test/lit/passes/optimize-instructions-gc-iit.wast
index e30fac8a8..cc1bb3b85 100644
--- a/test/lit/passes/optimize-instructions-gc-iit.wast
+++ b/test/lit/passes/optimize-instructions-gc-iit.wast
@@ -3,16 +3,22 @@
;; RUN: | filecheck %s
;; RUN: wasm-opt %s --optimize-instructions --ignore-implicit-traps --enable-reference-types --enable-gc --nominal -S -o - \
;; RUN: | filecheck %s --check-prefix NOMNL
+;; Also test trapsNeverHappen (with nominal; no need for both type system modes).
+;; RUN: wasm-opt %s --optimize-instructions --traps-never-happen --enable-reference-types --enable-gc --nominal -S -o - \
+;; RUN: | filecheck %s --check-prefix NOMNL-TNH
(module
;; CHECK: (type $parent (struct (field i32)))
;; NOMNL: (type $parent (struct (field i32)))
+ ;; NOMNL-TNH: (type $parent (struct (field i32)))
(type $parent (struct (field i32)))
;; CHECK: (type $child (struct (field i32) (field f64)))
;; NOMNL: (type $child (struct (field i32) (field f64)) (extends $parent))
+ ;; NOMNL-TNH: (type $child (struct (field i32) (field f64)) (extends $parent))
(type $child (struct (field i32) (field f64)) (extends $parent))
;; CHECK: (type $other (struct (field i64) (field f32)))
;; NOMNL: (type $other (struct (field i64) (field f32)))
+ ;; NOMNL-TNH: (type $other (struct (field i64) (field f32)))
(type $other (struct (field i64) (field f32)))
;; CHECK: (func $foo
@@ -21,6 +27,9 @@
;; NOMNL: (func $foo
;; NOMNL-NEXT: (nop)
;; NOMNL-NEXT: )
+ ;; NOMNL-TNH: (func $foo
+ ;; NOMNL-TNH-NEXT: (nop)
+ ;; NOMNL-TNH-NEXT: )
(func $foo)
@@ -84,6 +93,36 @@
;; NOMNL-NEXT: )
;; NOMNL-NEXT: )
;; NOMNL-NEXT: )
+ ;; NOMNL-TNH: (func $ref-cast-iit (param $parent (ref $parent)) (param $child (ref $child)) (param $other (ref $other)) (param $parent-rtt (rtt $parent)) (param $child-rtt (rtt $child)) (param $other-rtt (rtt $other))
+ ;; NOMNL-TNH-NEXT: (drop
+ ;; NOMNL-TNH-NEXT: (block (result (ref $parent))
+ ;; NOMNL-TNH-NEXT: (drop
+ ;; NOMNL-TNH-NEXT: (local.get $parent-rtt)
+ ;; NOMNL-TNH-NEXT: )
+ ;; NOMNL-TNH-NEXT: (local.get $parent)
+ ;; NOMNL-TNH-NEXT: )
+ ;; NOMNL-TNH-NEXT: )
+ ;; NOMNL-TNH-NEXT: (drop
+ ;; NOMNL-TNH-NEXT: (block (result (ref $child))
+ ;; NOMNL-TNH-NEXT: (drop
+ ;; NOMNL-TNH-NEXT: (local.get $parent-rtt)
+ ;; NOMNL-TNH-NEXT: )
+ ;; NOMNL-TNH-NEXT: (local.get $child)
+ ;; NOMNL-TNH-NEXT: )
+ ;; NOMNL-TNH-NEXT: )
+ ;; NOMNL-TNH-NEXT: (drop
+ ;; NOMNL-TNH-NEXT: (ref.cast
+ ;; NOMNL-TNH-NEXT: (local.get $parent)
+ ;; NOMNL-TNH-NEXT: (local.get $child-rtt)
+ ;; NOMNL-TNH-NEXT: )
+ ;; NOMNL-TNH-NEXT: )
+ ;; NOMNL-TNH-NEXT: (drop
+ ;; NOMNL-TNH-NEXT: (ref.cast
+ ;; NOMNL-TNH-NEXT: (local.get $child)
+ ;; NOMNL-TNH-NEXT: (local.get $other-rtt)
+ ;; NOMNL-TNH-NEXT: )
+ ;; NOMNL-TNH-NEXT: )
+ ;; NOMNL-TNH-NEXT: )
(func $ref-cast-iit
(param $parent (ref $parent))
(param $child (ref $child))
@@ -175,6 +214,32 @@
;; NOMNL-NEXT: )
;; NOMNL-NEXT: )
;; NOMNL-NEXT: )
+ ;; NOMNL-TNH: (func $ref-cast-iit-bad (param $parent (ref $parent)) (param $parent-rtt (rtt $parent))
+ ;; NOMNL-TNH-NEXT: (drop
+ ;; NOMNL-TNH-NEXT: (ref.cast
+ ;; NOMNL-TNH-NEXT: (block $block (result (ref $parent))
+ ;; NOMNL-TNH-NEXT: (call $foo)
+ ;; NOMNL-TNH-NEXT: (local.get $parent)
+ ;; NOMNL-TNH-NEXT: )
+ ;; NOMNL-TNH-NEXT: (block $block0 (result (rtt $parent))
+ ;; NOMNL-TNH-NEXT: (call $foo)
+ ;; NOMNL-TNH-NEXT: (local.get $parent-rtt)
+ ;; NOMNL-TNH-NEXT: )
+ ;; NOMNL-TNH-NEXT: )
+ ;; NOMNL-TNH-NEXT: )
+ ;; NOMNL-TNH-NEXT: (drop
+ ;; NOMNL-TNH-NEXT: (ref.cast
+ ;; NOMNL-TNH-NEXT: (local.get $parent)
+ ;; NOMNL-TNH-NEXT: (unreachable)
+ ;; NOMNL-TNH-NEXT: )
+ ;; NOMNL-TNH-NEXT: )
+ ;; NOMNL-TNH-NEXT: (drop
+ ;; NOMNL-TNH-NEXT: (ref.cast
+ ;; NOMNL-TNH-NEXT: (unreachable)
+ ;; NOMNL-TNH-NEXT: (local.get $parent-rtt)
+ ;; NOMNL-TNH-NEXT: )
+ ;; NOMNL-TNH-NEXT: )
+ ;; NOMNL-TNH-NEXT: )
(func $ref-cast-iit-bad
(param $parent (ref $parent))
(param $parent-rtt (rtt $parent))
@@ -218,6 +283,11 @@
;; NOMNL-NEXT: (i32.const 1)
;; NOMNL-NEXT: )
;; NOMNL-NEXT: )
+ ;; NOMNL-TNH: (func $ref-eq-ref-cast (param $x eqref)
+ ;; NOMNL-TNH-NEXT: (drop
+ ;; NOMNL-TNH-NEXT: (i32.const 1)
+ ;; NOMNL-TNH-NEXT: )
+ ;; NOMNL-TNH-NEXT: )
(func $ref-eq-ref-cast (param $x eqref)
;; we can look through a ref.cast if we ignore traps
(drop
diff --git a/test/lit/passes/vacuum-tnh.wast b/test/lit/passes/vacuum-tnh.wast
new file mode 100644
index 000000000..eaa63a4ae
--- /dev/null
+++ b/test/lit/passes/vacuum-tnh.wast
@@ -0,0 +1,74 @@
+;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited.
+;; RUN: wasm-opt %s --vacuum --traps-never-happen -all -S -o - | filecheck %s
+
+(module
+ (memory 1 1)
+
+ ;; CHECK: (func $drop (param $x i32) (param $y anyref)
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (unreachable)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $drop (param $x i32) (param $y anyref)
+ ;; A load might trap, normally, but if traps never happen then we can
+ ;; remove it.
+ (drop
+ (i32.load (local.get $x))
+ )
+
+ ;; A trap on a null value can also be ignored.
+ (drop
+ (ref.as_non_null
+ (local.get $y)
+ )
+ )
+
+ ;; Ignore unreachable code.
+ (drop
+ (unreachable)
+ )
+ )
+
+ ;; Other side effects prevent us making any changes.
+ ;; CHECK: (func $other-side-effects (param $x i32) (result i32)
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (call $other-side-effects
+ ;; CHECK-NEXT: (i32.const 1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (block $block (result i32)
+ ;; CHECK-NEXT: (local.set $x
+ ;; CHECK-NEXT: (i32.const 2)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (i32.load
+ ;; CHECK-NEXT: (local.get $x)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (i32.const 1)
+ ;; CHECK-NEXT: )
+ (func $other-side-effects (param $x i32) (result i32)
+ ;; A call has all manner of other side effects.
+ (drop
+ (call $other-side-effects (i32.const 1))
+ )
+
+ ;; Add to the load an additional specific side effect, of writing to a
+ ;; local.
+ (drop
+ (block (result i32)
+ (local.set $x (i32.const 2))
+ (i32.load (local.get $x))
+ )
+ )
+
+ (i32.const 1)
+ )
+
+ ;; A helper function for the above, that returns nothing.
+ ;; CHECK: (func $return-nothing
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: )
+ (func $return-nothing)
+)