summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/passes/GlobalEffects.cpp189
-rw-r--r--test/lit/passes/global-effects.wast121
-rw-r--r--test/lit/passes/global-effects_simplify-locals.wast113
3 files changed, 386 insertions, 37 deletions
diff --git a/src/passes/GlobalEffects.cpp b/src/passes/GlobalEffects.cpp
index a2c34b644..47c4d54ab 100644
--- a/src/passes/GlobalEffects.cpp
+++ b/src/passes/GlobalEffects.cpp
@@ -22,60 +22,183 @@
#include "ir/effects.h"
#include "ir/module-utils.h"
#include "pass.h"
+#include "support/unique_deferring_queue.h"
#include "wasm.h"
namespace wasm {
struct GenerateGlobalEffects : public Pass {
void run(Module* module) override {
- // TODO: Full transitive closure of effects. For now, we just look at each
- // function by itself.
+ // First, we do a scan of each function to see what effects they have,
+ // including which functions they call directly (so that we can compute
+ // transitive effects later).
- auto& funcEffectsMap = getPassOptions().funcEffectsMap;
-
- // First, clear any previous effects.
- funcEffectsMap.reset();
+ struct FuncInfo {
+ // Effects in this function.
+ std::optional<EffectAnalyzer> effects;
- // When we find useful effects, we'll save them. If we can't find anything,
- // the final map we emit will not have an entry there at all.
- using PossibleEffects = std::unique_ptr<EffectAnalyzer>;
+ // Directly-called functions from this function.
+ std::unordered_set<Name> calledFunctions;
+ };
- ModuleUtils::ParallelFunctionAnalysis<PossibleEffects> analysis(
- *module, [&](Function* func, PossibleEffects& storedEffects) {
+ ModuleUtils::ParallelFunctionAnalysis<FuncInfo> analysis(
+ *module, [&](Function* func, FuncInfo& funcInfo) {
if (func->imported()) {
// Imports can do anything, so we need to assume the worst anyhow,
// which is the same as not specifying any effects for them in the
- // map.
+ // map (which we do by not setting funcInfo.effects).
return;
}
// Gather the effects.
- auto effects =
- std::make_unique<EffectAnalyzer>(getPassOptions(), *module, func);
-
- // If the body has a call, give up - that means we can't infer a more
- // specific set of effects than the pessimistic case of just assuming
- // any call to here is an arbitrary call. (See the TODO above for
- // improvements.)
- if (effects->calls) {
- return;
+ funcInfo.effects.emplace(getPassOptions(), *module, func);
+
+ if (funcInfo.effects->calls) {
+ // There are calls in this function, which we will analyze in detail.
+ // Clear the |calls| field first, and we'll handle calls of all sorts
+ // below.
+ funcInfo.effects->calls = false;
+
+ // Clear throws as well, as we are "forgetting" calls right now, and
+ // want to forget their throwing effect as well. If we see something
+ // else that throws, below, then we'll note that there.
+ funcInfo.effects->throws_ = false;
+
+ struct CallScanner
+ : public PostWalker<CallScanner,
+ UnifiedExpressionVisitor<CallScanner>> {
+ Module& wasm;
+ PassOptions& options;
+ FuncInfo& funcInfo;
+
+ CallScanner(Module& wasm, PassOptions& options, FuncInfo& funcInfo)
+ : wasm(wasm), options(options), funcInfo(funcInfo) {}
+
+ void visitExpression(Expression* curr) {
+ ShallowEffectAnalyzer effects(options, wasm, curr);
+ if (auto* call = curr->dynCast<Call>()) {
+ // Note the direct call.
+ funcInfo.calledFunctions.insert(call->target);
+ } else if (effects.calls) {
+ // This is an indirect call of some sort, so we must assume the
+ // worst. To do so, clear the effects, which indicates nothing
+ // is known (so anything is possible).
+ // TODO: We could group effects by function type etc.
+ funcInfo.effects.reset();
+ } else {
+ // No call here, but update throwing if we see it. (Only do so,
+ // however, if we have effects; if we cleared it - see before -
+ // then we assume the worst anyhow, and have nothing to update.)
+ if (effects.throws_ && funcInfo.effects) {
+ funcInfo.effects->throws_ = true;
+ }
+ }
+ }
+ };
+ CallScanner scanner(*module, getPassOptions(), funcInfo);
+ scanner.walkFunction(func);
}
-
- // Save the useful effects we found.
- storedEffects = std::move(effects);
});
- // Generate the final data structure.
- for (auto& [func, possibleEffects] : analysis.map) {
- if (possibleEffects) {
- // Only allocate a new funcEffectsMap if we actually have data for it
- // (which might make later effect computation slightly faster, to
- // quickly skip the funcEffectsMap code path).
- if (!funcEffectsMap) {
- funcEffectsMap = std::make_shared<FuncEffectsMap>();
+ // Compute the transitive closure of effects. To do so, first construct for
+ // each function a list of the functions that it is called by (so we need to
+ // propogate its effects to them), and then we'll construct the closure of
+ // that.
+ //
+ // callers[foo] = [func that calls foo, another func that calls foo, ..]
+ //
+ std::unordered_map<Name, std::unordered_set<Name>> callers;
+
+ // Our work queue contains info about a new call pair: a call from a caller
+ // to a called function, that is information we then apply and propagate.
+ using CallPair = std::pair<Name, Name>; // { caller, called }
+ UniqueDeferredQueue<CallPair> work;
+ for (auto& [func, info] : analysis.map) {
+ for (auto& called : info.calledFunctions) {
+ work.push({func->name, called});
+ }
+ }
+
+ // Compute the transitive closure of the call graph, that is, fill out
+ // |callers| so that it contains the list of all callers - even through a
+ // chain - of each function.
+ while (!work.empty()) {
+ auto [caller, called] = work.pop();
+
+ // We must not already have an entry for this call (that would imply we
+ // are doing wasted work).
+ assert(!callers[called].count(caller));
+
+ // Apply the new call information.
+ callers[called].insert(caller);
+
+ // We just learned that |caller| calls |called|. It also calls
+ // transitively, which we need to propagate to all places unaware of that
+ // information yet.
+ //
+ // caller => called => called by called
+ //
+ auto& calledInfo = analysis.map[module->getFunction(called)];
+ for (auto calledByCalled : calledInfo.calledFunctions) {
+ if (!callers[calledByCalled].count(caller)) {
+ work.push({caller, calledByCalled});
+ }
+ }
+ }
+
+ // Now that we have transitively propagated all static calls, apply that
+ // information. First, apply infinite recursion: if a function can call
+ // itself then it might recurse infinitely, which we consider an effect (a
+ // trap).
+ for (auto& [func, info] : analysis.map) {
+ if (callers[func->name].count(func->name)) {
+ if (info.effects) {
+ info.effects->trap = true;
+ }
+ }
+ }
+
+ // Next, apply function effects to their callers.
+ for (auto& [func, info] : analysis.map) {
+ auto& funcEffects = info.effects;
+
+ for (auto& caller : callers[func->name]) {
+ auto& callerEffects = analysis.map[module->getFunction(caller)].effects;
+ if (!callerEffects) {
+ // Nothing is known for the caller, which is already the worst case.
+ continue;
}
- funcEffectsMap->emplace(func->name, *possibleEffects);
+
+ if (!funcEffects) {
+ // Nothing is known for the called function, which means nothing is
+ // known for the caller either.
+ callerEffects.reset();
+ continue;
+ }
+
+ // Add func's effects to the caller.
+ callerEffects->mergeIn(*funcEffects);
+ }
+ }
+
+ // Generate the final data structure, starting from a blank slate where
+ // nothing is known.
+ auto& funcEffectsMap = getPassOptions().funcEffectsMap;
+ funcEffectsMap.reset();
+
+ for (auto& [func, info] : analysis.map) {
+ if (!info.effects) {
+ // Add no entry to funcEffectsMap, since nothing is known.
+ continue;
+ }
+
+ // Only allocate a new funcEffectsMap here when we actually have data for
+ // it (which might make later effect computation slightly faster, to
+ // quickly skip the funcEffectsMap code path).
+ if (!funcEffectsMap) {
+ funcEffectsMap = std::make_shared<FuncEffectsMap>();
}
+ funcEffectsMap->emplace(func->name, *info.effects);
}
}
};
diff --git a/test/lit/passes/global-effects.wast b/test/lit/passes/global-effects.wast
index e5ea1b429..066604679 100644
--- a/test/lit/passes/global-effects.wast
+++ b/test/lit/passes/global-effects.wast
@@ -8,12 +8,15 @@
;; RUN: foreach %s %t wasm-opt -all --generate-global-effects --discard-global-effects --vacuum -S -o - | filecheck %s --check-prefix DISCARD
(module
+
;; WITHOUT: (type $0 (func))
;; WITHOUT: (type $1 (func (result i32)))
;; WITHOUT: (type $2 (func (param i32)))
+ ;; WITHOUT: (import "a" "b" (func $import (type $0)))
+
;; WITHOUT: (tag $tag)
;; INCLUDE: (type $0 (func))
@@ -21,6 +24,8 @@
;; INCLUDE: (type $2 (func (param i32)))
+ ;; INCLUDE: (import "a" "b" (func $import (type $0)))
+
;; INCLUDE: (tag $tag)
;; DISCARD: (type $0 (func))
@@ -28,9 +33,13 @@
;; DISCARD: (type $2 (func (param i32)))
+ ;; DISCARD: (import "a" "b" (func $import (type $0)))
+
;; DISCARD: (tag $tag)
(tag $tag)
+ (import "a" "b" (func $import))
+
;; WITHOUT: (func $main (type $0)
;; WITHOUT-NEXT: (call $nop)
;; WITHOUT-NEXT: (call $unreachable)
@@ -39,11 +48,14 @@
;; WITHOUT-NEXT: (drop
;; WITHOUT-NEXT: (call $unimportant-effects)
;; WITHOUT-NEXT: )
+ ;; WITHOUT-NEXT: (call $throw)
+ ;; WITHOUT-NEXT: (call $throw-and-import)
;; WITHOUT-NEXT: )
;; INCLUDE: (func $main (type $0)
;; INCLUDE-NEXT: (call $unreachable)
- ;; INCLUDE-NEXT: (call $call-nop)
;; INCLUDE-NEXT: (call $call-unreachable)
+ ;; INCLUDE-NEXT: (call $throw)
+ ;; INCLUDE-NEXT: (call $throw-and-import)
;; INCLUDE-NEXT: )
;; DISCARD: (func $main (type $0)
;; DISCARD-NEXT: (call $nop)
@@ -53,6 +65,8 @@
;; DISCARD-NEXT: (drop
;; DISCARD-NEXT: (call $unimportant-effects)
;; DISCARD-NEXT: )
+ ;; DISCARD-NEXT: (call $throw)
+ ;; DISCARD-NEXT: (call $throw-and-import)
;; DISCARD-NEXT: )
(func $main
;; Calling a function with no effects can be optimized away in INCLUDE (but
@@ -61,8 +75,7 @@
;; Calling a function with effects cannot.
(call $unreachable)
;; Calling something that calls something with no effects can be optimized
- ;; away in principle, but atm we don't look that far, so this is not
- ;; optimized.
+ ;; away, since we compute transitive effects
(call $call-nop)
;; Calling something that calls something with effects cannot.
(call $call-unreachable)
@@ -71,6 +84,10 @@
(drop
(call $unimportant-effects)
)
+ ;; A throwing function cannot be removed.
+ (call $throw)
+ ;; A function that throws and calls an import definitely cannot be removed.
+ (call $throw-and-import)
)
;; WITHOUT: (func $cycle (type $0)
@@ -88,6 +105,33 @@
(call $cycle)
)
+ ;; WITHOUT: (func $cycle-1 (type $0)
+ ;; WITHOUT-NEXT: (call $cycle-2)
+ ;; WITHOUT-NEXT: )
+ ;; INCLUDE: (func $cycle-1 (type $0)
+ ;; INCLUDE-NEXT: (call $cycle-2)
+ ;; INCLUDE-NEXT: )
+ ;; DISCARD: (func $cycle-1 (type $0)
+ ;; DISCARD-NEXT: (call $cycle-2)
+ ;; DISCARD-NEXT: )
+ (func $cycle-1
+ ;; $cycle-1 and -2 form a cycle together, in which no call can be removed.
+ (call $cycle-2)
+ )
+
+ ;; WITHOUT: (func $cycle-2 (type $0)
+ ;; WITHOUT-NEXT: (call $cycle-1)
+ ;; WITHOUT-NEXT: )
+ ;; INCLUDE: (func $cycle-2 (type $0)
+ ;; INCLUDE-NEXT: (call $cycle-1)
+ ;; INCLUDE-NEXT: )
+ ;; DISCARD: (func $cycle-2 (type $0)
+ ;; DISCARD-NEXT: (call $cycle-1)
+ ;; DISCARD-NEXT: )
+ (func $cycle-2
+ (call $cycle-1)
+ )
+
;; WITHOUT: (func $nop (type $0)
;; WITHOUT-NEXT: (nop)
;; WITHOUT-NEXT: )
@@ -190,9 +234,24 @@
;; WITHOUT-NEXT: (nop)
;; WITHOUT-NEXT: )
;; WITHOUT-NEXT: )
+ ;; WITHOUT-NEXT: (try $try0
+ ;; WITHOUT-NEXT: (do
+ ;; WITHOUT-NEXT: (call $throw-and-import)
+ ;; WITHOUT-NEXT: )
+ ;; WITHOUT-NEXT: (catch_all
+ ;; WITHOUT-NEXT: (nop)
+ ;; WITHOUT-NEXT: )
+ ;; WITHOUT-NEXT: )
;; WITHOUT-NEXT: )
;; INCLUDE: (func $call-throw-and-catch (type $0)
- ;; INCLUDE-NEXT: (nop)
+ ;; INCLUDE-NEXT: (try $try0
+ ;; INCLUDE-NEXT: (do
+ ;; INCLUDE-NEXT: (call $throw-and-import)
+ ;; INCLUDE-NEXT: )
+ ;; INCLUDE-NEXT: (catch_all
+ ;; INCLUDE-NEXT: (nop)
+ ;; INCLUDE-NEXT: )
+ ;; INCLUDE-NEXT: )
;; INCLUDE-NEXT: )
;; DISCARD: (func $call-throw-and-catch (type $0)
;; DISCARD-NEXT: (try $try
@@ -203,6 +262,14 @@
;; DISCARD-NEXT: (nop)
;; DISCARD-NEXT: )
;; DISCARD-NEXT: )
+ ;; DISCARD-NEXT: (try $try0
+ ;; DISCARD-NEXT: (do
+ ;; DISCARD-NEXT: (call $throw-and-import)
+ ;; DISCARD-NEXT: )
+ ;; DISCARD-NEXT: (catch_all
+ ;; DISCARD-NEXT: (nop)
+ ;; DISCARD-NEXT: )
+ ;; DISCARD-NEXT: )
;; DISCARD-NEXT: )
(func $call-throw-and-catch
(try
@@ -214,6 +281,13 @@
)
(catch_all)
)
+ (try
+ (do
+ ;; This call both throws and calls an import, and cannot be removed.
+ (call $throw-and-import)
+ )
+ (catch_all)
+ )
)
;; WITHOUT: (func $call-unreachable-and-catch (type $0)
@@ -320,4 +394,43 @@
(func $throw
(throw $tag)
)
+
+ ;; WITHOUT: (func $throw-and-import (type $0)
+ ;; WITHOUT-NEXT: (throw $tag)
+ ;; WITHOUT-NEXT: )
+ ;; INCLUDE: (func $throw-and-import (type $0)
+ ;; INCLUDE-NEXT: (throw $tag)
+ ;; INCLUDE-NEXT: )
+ ;; DISCARD: (func $throw-and-import (type $0)
+ ;; DISCARD-NEXT: (throw $tag)
+ ;; DISCARD-NEXT: )
+ (func $throw-and-import
+ (if
+ (i32.const 1)
+ (throw $tag)
+ (call $import)
+ )
+ )
+
+ ;; WITHOUT: (func $cycle-with-unknown-call (type $0)
+ ;; WITHOUT-NEXT: (call $cycle-with-unknown-call)
+ ;; WITHOUT-NEXT: (call $import)
+ ;; WITHOUT-NEXT: )
+ ;; INCLUDE: (func $cycle-with-unknown-call (type $0)
+ ;; INCLUDE-NEXT: (call $cycle-with-unknown-call)
+ ;; INCLUDE-NEXT: (call $import)
+ ;; INCLUDE-NEXT: )
+ ;; DISCARD: (func $cycle-with-unknown-call (type $0)
+ ;; DISCARD-NEXT: (call $cycle-with-unknown-call)
+ ;; DISCARD-NEXT: (call $import)
+ ;; DISCARD-NEXT: )
+ (func $cycle-with-unknown-call
+ ;; This function can not only call itself recursively, but also calls an
+ ;; import. We should not remove anything here, and not error during the
+ ;; analysis (this guards against a bug where the import would make us toss
+ ;; away the effects object, and the infinite loop makes us set a property on
+ ;; that object, so it must check the object still exists).
+ (call $cycle-with-unknown-call)
+ (call $import)
+ )
)
diff --git a/test/lit/passes/global-effects_simplify-locals.wast b/test/lit/passes/global-effects_simplify-locals.wast
new file mode 100644
index 000000000..3053c59f0
--- /dev/null
+++ b/test/lit/passes/global-effects_simplify-locals.wast
@@ -0,0 +1,113 @@
+;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited.
+
+;; Test that global effects let us optimize more in a pass like simplify-locals.
+
+;; RUN: foreach %s %t wasm-opt --simplify-locals -S -o - | filecheck %s --check-prefix NORMAL_EFFECTS
+;; RUN: foreach %s %t wasm-opt --generate-global-effects --simplify-locals -S -o - | filecheck %s --check-prefix GLOBAL_EFFECTS
+
+(module
+ ;; NORMAL_EFFECTS: (type $0 (func))
+
+ ;; NORMAL_EFFECTS: (global $global (mut i32) (i32.const 0))
+ ;; GLOBAL_EFFECTS: (type $0 (func))
+
+ ;; GLOBAL_EFFECTS: (global $global (mut i32) (i32.const 0))
+ (global $global (mut i32) (i32.const 0))
+
+ ;; NORMAL_EFFECTS: (func $past-get
+ ;; NORMAL_EFFECTS-NEXT: (local $x i32)
+ ;; NORMAL_EFFECTS-NEXT: (local.set $x
+ ;; NORMAL_EFFECTS-NEXT: (global.get $global)
+ ;; NORMAL_EFFECTS-NEXT: )
+ ;; NORMAL_EFFECTS-NEXT: (call $get-global)
+ ;; NORMAL_EFFECTS-NEXT: (drop
+ ;; NORMAL_EFFECTS-NEXT: (local.get $x)
+ ;; NORMAL_EFFECTS-NEXT: )
+ ;; NORMAL_EFFECTS-NEXT: )
+ ;; GLOBAL_EFFECTS: (func $past-get
+ ;; GLOBAL_EFFECTS-NEXT: (local $x i32)
+ ;; GLOBAL_EFFECTS-NEXT: (nop)
+ ;; GLOBAL_EFFECTS-NEXT: (call $get-global)
+ ;; GLOBAL_EFFECTS-NEXT: (drop
+ ;; GLOBAL_EFFECTS-NEXT: (global.get $global)
+ ;; GLOBAL_EFFECTS-NEXT: )
+ ;; GLOBAL_EFFECTS-NEXT: )
+ (func $past-get
+ (local $x i32)
+ ;; We can move this set past the call (when we have global effects), since
+ ;; the call only reads the global.
+ (local.set $x
+ (global.get $global)
+ )
+ (call $get-global)
+ (drop
+ (local.get $x)
+ )
+ )
+
+ ;; NORMAL_EFFECTS: (func $past-set
+ ;; NORMAL_EFFECTS-NEXT: (local $x i32)
+ ;; NORMAL_EFFECTS-NEXT: (local.set $x
+ ;; NORMAL_EFFECTS-NEXT: (global.get $global)
+ ;; NORMAL_EFFECTS-NEXT: )
+ ;; NORMAL_EFFECTS-NEXT: (call $set-global)
+ ;; NORMAL_EFFECTS-NEXT: (drop
+ ;; NORMAL_EFFECTS-NEXT: (local.get $x)
+ ;; NORMAL_EFFECTS-NEXT: )
+ ;; NORMAL_EFFECTS-NEXT: )
+ ;; GLOBAL_EFFECTS: (func $past-set
+ ;; GLOBAL_EFFECTS-NEXT: (local $x i32)
+ ;; GLOBAL_EFFECTS-NEXT: (local.set $x
+ ;; GLOBAL_EFFECTS-NEXT: (global.get $global)
+ ;; GLOBAL_EFFECTS-NEXT: )
+ ;; GLOBAL_EFFECTS-NEXT: (call $set-global)
+ ;; GLOBAL_EFFECTS-NEXT: (drop
+ ;; GLOBAL_EFFECTS-NEXT: (local.get $x)
+ ;; GLOBAL_EFFECTS-NEXT: )
+ ;; GLOBAL_EFFECTS-NEXT: )
+ (func $past-set
+ (local $x i32)
+ ;; We cannot move this set past the call, since the call writes the global.
+ (local.set $x
+ (global.get $global)
+ )
+ (call $set-global)
+ (drop
+ (local.get $x)
+ )
+ )
+
+ ;; NORMAL_EFFECTS: (func $set-global
+ ;; NORMAL_EFFECTS-NEXT: (global.set $global
+ ;; NORMAL_EFFECTS-NEXT: (i32.const 1)
+ ;; NORMAL_EFFECTS-NEXT: )
+ ;; NORMAL_EFFECTS-NEXT: )
+ ;; GLOBAL_EFFECTS: (func $set-global
+ ;; GLOBAL_EFFECTS-NEXT: (global.set $global
+ ;; GLOBAL_EFFECTS-NEXT: (i32.const 1)
+ ;; GLOBAL_EFFECTS-NEXT: )
+ ;; GLOBAL_EFFECTS-NEXT: )
+ (func $set-global
+ ;; Helper function.
+ (global.set $global
+ (i32.const 1)
+ )
+ )
+
+ ;; NORMAL_EFFECTS: (func $get-global
+ ;; NORMAL_EFFECTS-NEXT: (drop
+ ;; NORMAL_EFFECTS-NEXT: (global.get $global)
+ ;; NORMAL_EFFECTS-NEXT: )
+ ;; NORMAL_EFFECTS-NEXT: )
+ ;; GLOBAL_EFFECTS: (func $get-global
+ ;; GLOBAL_EFFECTS-NEXT: (drop
+ ;; GLOBAL_EFFECTS-NEXT: (global.get $global)
+ ;; GLOBAL_EFFECTS-NEXT: )
+ ;; GLOBAL_EFFECTS-NEXT: )
+ (func $get-global
+ ;; Helper function.
+ (drop
+ (global.get $global)
+ )
+ )
+)