summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/passes/SimplifyGlobals.cpp172
1 files changed, 158 insertions, 14 deletions
diff --git a/src/passes/SimplifyGlobals.cpp b/src/passes/SimplifyGlobals.cpp
index b57490d3c..5a6337bd6 100644
--- a/src/passes/SimplifyGlobals.cpp
+++ b/src/passes/SimplifyGlobals.cpp
@@ -25,6 +25,8 @@
// * Apply the constant values of previous global.sets, in a linear
// execution trace.
// * Remove writes to globals that are never read from.
+// * Remove writes to globals that are only read from in order to write (see
+// below, "readOnlyToWrite").
//
// Some globals may not have uses after these changes, which we leave
// to other passes to optimize.
@@ -50,10 +52,36 @@ namespace wasm {
namespace {
struct GlobalInfo {
+ // Whether the global is imported and exported.
bool imported = false;
bool exported = false;
- std::atomic<bool> written;
- std::atomic<bool> read;
+
+ // How many times the global is written and read.
+ std::atomic<Index> written{0};
+ std::atomic<Index> read{0};
+
+ // How many times the global is "read, but only to write", that is, is used in
+ // this pattern:
+ //
+ // if (global == X) { global = Y }
+ //
+ // where X and Y have no side effects. If all we have are such reads only to
+ // write then the global is really not necessary, even though there are both
+ // reads and writes of it.
+ //
+ // This pattern can show up in global initialization code, where in the block
+ // alongside "global = Y" there was some useful code, but the optimizer
+ // managed to remove it. For example,
+ //
+ // if (global == 0) { global = 1; sideEffect(); }
+ //
+ // If the global's initial value is the default 0, and there are no other uses
+ // of this global, then this code will run sideEffect() the very first time we
+ // reach here. We therefore need to keep this global and its reads and writes.
+ // However, if sideEffect() were removed, then we read the global only to
+ // write it - and nothing else - and so we can optimize away that global
+ // entirely.
+ std::atomic<Index> readOnlyToWrite{0};
};
using GlobalInfoMap = std::map<Name, GlobalInfo>;
@@ -65,9 +93,53 @@ struct GlobalUseScanner : public WalkerPass<PostWalker<GlobalUseScanner>> {
GlobalUseScanner* create() override { return new GlobalUseScanner(infos); }
- void visitGlobalSet(GlobalSet* curr) { (*infos)[curr->name].written = true; }
+ void visitGlobalSet(GlobalSet* curr) { (*infos)[curr->name].written++; }
+
+ void visitGlobalGet(GlobalGet* curr) { (*infos)[curr->name].read++; }
+
+ void visitIf(If* curr) {
+ // We are looking for
+ //
+ // if (global == X) { global = Y }
+ //
+ // Ignore an if-else, which cannot be that.
+ if (curr->ifFalse) {
+ return;
+ }
+
+ // See if reading a specific global is the only effect the condition has.
+ EffectAnalyzer condition(
+ getPassOptions(), getModule()->features, curr->condition);
+
+ if (condition.globalsRead.size() != 1) {
+ return;
+ }
+ auto global = *condition.globalsRead.begin();
+ condition.globalsRead.clear();
+ if (condition.hasAnything()) {
+ return;
+ }
+
+ // See if writing the same global is the only effect the body has. (Note
+ // that we don't need to care about the case where the body has no effects
+ // at all - other pass would handle that trivial situation.)
+ EffectAnalyzer ifTrue(
+ getPassOptions(), getModule()->features, curr->ifTrue);
+ if (ifTrue.globalsWritten.size() != 1) {
+ return;
+ }
+ auto writtenGlobal = *ifTrue.globalsWritten.begin();
+ if (writtenGlobal != global) {
+ return;
+ }
+ ifTrue.globalsWritten.clear();
+ if (ifTrue.hasAnything()) {
+ return;
+ }
- void visitGlobalGet(GlobalGet* curr) { (*infos)[curr->name].read = true; }
+ // This is exactly the pattern we sought!
+ (*infos)[global].readOnlyToWrite++;
+ }
private:
GlobalInfoMap* infos;
@@ -215,18 +287,29 @@ struct SimplifyGlobals : public Pass {
runner = runner_;
module = module_;
+ while (iteration()) {
+ }
+ }
+
+ bool iteration() {
analyze();
- removeWritesToUnreadGlobals();
+ // Removing unneeded writes can in some cases lead to more optimizations
+ // that we need an entire additional iteration to perform, see below.
+ bool more = removeUnneededWrites();
preferEarlierImports();
propagateConstantsToGlobals();
propagateConstantsToCode();
+
+ return more;
}
void analyze() {
+ map.clear();
+
// First, find out all the relevant info.
for (auto& global : module->globals) {
auto& info = map[global->name];
@@ -250,21 +333,82 @@ struct SimplifyGlobals : public Pass {
}
}
- void removeWritesToUnreadGlobals() {
- // Globals that are not exports and not read from can be eliminated
- // (even if they are written to).
- NameSet unreadGlobals;
+ // Removes writes from globals that will never do anything useful with the
+ // written value anyhow. Returns whether an addition iteration is necessary.
+ bool removeUnneededWrites() {
+ bool more = false;
+
+ // Globals that are not exports and not read from are unnecessary (even if
+ // they are written to). Likewise, globals that are only read from in order
+ // to write to themselves are unnecessary. First, find such globals.
+ NameSet unnecessaryGlobals;
for (auto& global : module->globals) {
auto& info = map[global->name];
- if (!info.imported && !info.exported && !info.read) {
- unreadGlobals.insert(global->name);
+
+ if (!info.written) {
+ // No writes occur here, so there is nothing for us to remove.
+ continue;
+ }
+
+ if (info.imported || info.exported) {
+ // If the global is observable from the outside, we can't do anythng
+ // here.
+ //
+ // TODO: optimize the case of an imported but immutable global, etc.
+ continue;
+ }
+
+ // We only ever optimize read-only-to-write if all of our reads are done
+ // in places we identified as read-only-to-write. That is, we have
+ // eliminated the possibility of any other uses. (Technically, each
+ // read-to-write location might have more than one read since we did not
+ // count them, but only verified there was one read or more; but this is
+ // good enough as the common case has exactly one.)
+ //
+ // Note that there might be more writes, if there are additional writes
+ // besides those in the read-only-to-write locations. But we can ignore
+ // those, as whatever they write will not be read in order to do anything
+ // of value.
+ bool onlyReadOnlyToWrite = (info.read == info.readOnlyToWrite);
+
+ // There is at least one write in each read-only-to-write location, unless
+ // our logic is wrong somewhere.
+ assert(info.written >= info.readOnlyToWrite);
+
+ if (!info.read || onlyReadOnlyToWrite) {
+ unnecessaryGlobals.insert(global->name);
+
// We can now mark this global as immutable, and un-written, since we
- // are about to remove all the `.set` operations on it.
+ // are about to remove all the operations on it.
global->mutable_ = false;
- info.written = false;
+ info.written = 0;
+
+ // Nested old-read-to-write expressions require another full iteration
+ // to optimize, as we have:
+ //
+ // if (a) {
+ // a = 1;
+ // if (b) {
+ // b = 1;
+ // }
+ // }
+ //
+ // The first iteration can only optimize b, as the outer if's body has
+ // more effects than we understand. After finishing the first iteration,
+ // b will no longer exist, removing those effects.
+ if (onlyReadOnlyToWrite) {
+ more = true;
+ }
}
}
- GlobalSetRemover(&unreadGlobals, optimize).run(runner, module);
+
+ // Remove all the sets on the unnecessary globals. Later optimizations can
+ // then see that since the global has no writes, it is a constant, which
+ // will lead to removal of gets, and after removing them, the global itself
+ // will be removed as well.
+ GlobalSetRemover(&unnecessaryGlobals, optimize).run(runner, module);
+
+ return more;
}
void preferEarlierImports() {