summaryrefslogtreecommitdiff
path: root/src/tools/wasm-reduce.cpp
diff options
context:
space:
mode:
authorAlon Zakai <azakai@google.com>2021-07-20 13:04:28 -0700
committerGitHub <noreply@github.com>2021-07-20 13:04:28 -0700
commit1e79c53ec1ad3d80db3354f15919331fdfa7ed28 (patch)
tree7cb1e8ef4b7bacd3efa6e6085a3344b41efefcd5 /src/tools/wasm-reduce.cpp
parente67d50d5a6290ec9968344ee5712ebf62847fea8 (diff)
downloadbinaryen-1e79c53ec1ad3d80db3354f15919331fdfa7ed28.tar.gz
binaryen-1e79c53ec1ad3d80db3354f15919331fdfa7ed28.tar.bz2
binaryen-1e79c53ec1ad3d80db3354f15919331fdfa7ed28.zip
Exponentially empty out function bodies when reducing (#3997)
This removes the code that did so one at a time, and instead adds it in a way that we can do it in an exponentially growing set of functions. On large testcases where other methods do not work, this is very useful. Also adjust the factor to do this 20x more often, which in practice is very useful too.
Diffstat (limited to 'src/tools/wasm-reduce.cpp')
-rw-r--r--src/tools/wasm-reduce.cpp120
1 files changed, 75 insertions, 45 deletions
diff --git a/src/tools/wasm-reduce.cpp b/src/tools/wasm-reduce.cpp
index ed2b9b564..747b93b95 100644
--- a/src/tools/wasm-reduce.cpp
+++ b/src/tools/wasm-reduce.cpp
@@ -330,8 +330,8 @@ struct Reducer
loadWorking();
reduced = 0;
funcsSeen = 0;
- // before we do any changes, it should be valid to write out the module:
- // size should be as expected, and output should be as expected
+ // Before we do any changes, it should be valid to write out the module:
+ // size should be as expected, and output should be as expected.
ProgramResult result;
if (!writeAndTestReduction(result)) {
std::cerr << "\n|! WARNING: writing before destructive reduction fails, "
@@ -475,35 +475,6 @@ struct Reducer
// since we don't need to duplicate work that they do
void visitExpression(Expression* curr) {
- if (getFunction() && curr == getFunction()->body) {
- // At the top level, we can try to reduce anything to an unreachable or a
- // nop, and it is useful to do so when possible.
- if (!curr->is<Unreachable>() && !curr->is<Nop>() &&
- shouldTryToReduce(1000)) {
- auto* save = curr;
- Unreachable un;
- Nop nop;
- bool useUnreachable = getFunction()->getResults() != Type::none;
- if (useUnreachable) {
- replaceCurrent(&un);
- } else {
- replaceCurrent(&nop);
- }
- if (writeAndTestReduction()) {
- if (useUnreachable) {
- replaceCurrent(builder->makeUnreachable());
- } else {
- replaceCurrent(builder->makeNop());
- }
- std::cerr << "| body emptied (" << getFunction()->name
- << ")\n";
- noteReduction();
- return;
- } else {
- replaceCurrent(save);
- }
- }
- }
// type-based reductions
if (curr->type == Type::none) {
if (tryToReduceCurrentToNop()) {
@@ -842,7 +813,7 @@ struct Reducer
return shrank;
}
- void shrinkElementSegments(Module* module) {
+ void shrinkElementSegments() {
std::cerr << "| try to simplify elem segments\n";
Expression* first = nullptr;
auto it =
@@ -887,11 +858,9 @@ struct Reducer
}
}
- void visitModule(Module* curr) {
- assert(curr == module.get());
-
- shrinkElementSegments(curr);
-
+ // Reduces entire functions at a time. Returns whether we did a significant
+ // amount of reduction that justifies doing even more.
+ bool reduceFunctions() {
// try to remove functions
std::cerr << "| try to remove functions\n";
std::vector<Name> functionNames;
@@ -899,13 +868,14 @@ struct Reducer
functionNames.push_back(func->name);
}
size_t skip = 1;
+ size_t maxSkip = 1;
// If we just removed some functions in the previous iteration, keep trying
// to remove more as this is one of the most efficient ways to reduce.
- bool justRemoved = false;
+ bool justReduced = true;
for (size_t i = 0; i < functionNames.size(); i++) {
- if (!justRemoved &&
+ if (!justReduced &&
functionsWeTriedToRemove.count(functionNames[i]) == 1 &&
- !shouldTryToReduce(std::max((factor / 100) + 1, 1000))) {
+ !shouldTryToReduce(std::max((factor / 5) + 1, 20000))) {
continue;
}
std::vector<Name> names;
@@ -920,25 +890,48 @@ struct Reducer
if (names.size() == 0) {
continue;
}
- std::cout << "| try to remove " << names.size()
- << " functions (skip: " << skip << ")\n";
- justRemoved = tryToRemoveFunctions(names);
- if (justRemoved) {
+ // Try to remove functions, and if that fails, try to at least empty out
+ // their bodies.
+ justReduced = tryToRemoveFunctions(names) || tryToEmptyFunctions(names);
+ if (justReduced) {
noteReduction(names.size());
i += skip;
skip = std::min(size_t(factor), 2 * skip);
+ maxSkip = std::max(skip, maxSkip);
} else {
skip = std::max(skip / 2, size_t(1)); // or 1?
i += factor / 100;
}
}
+ // If maxSkip is 1 then we never reduced at all. If it is 2 then we did
+ // manage to reduce individual functions, but all our attempts at
+ // exponential growth failed. Only suggest doing a new iteration of this
+ // function if we did in fact manage to grow, which indicated there are lots
+ // of opportunities here, and it is worth focusing on this.
+ return maxSkip > 2;
+ }
+
+ void visitModule(Module* curr) {
+ // The initial module given to us is our global object. As we continue to
+ // process things here, we may replace the module, so we should never again
+ // refer to curr.
+ assert(curr == module.get());
+ curr = nullptr;
+
+ // Reduction of entire functions at a time is very effective, and we do it
+ // with exponential growth and backoff, so keep doing it while it works.
+ while (reduceFunctions()) {
+ }
+
+ shrinkElementSegments();
+
// try to remove exports
std::cerr << "| try to remove exports (with factor " << factor << ")\n";
std::vector<Export> exports;
for (auto& exp : module->exports) {
exports.push_back(*exp);
}
- skip = 1;
+ size_t skip = 1;
for (size_t i = 0; i < exports.size(); i++) {
if (!shouldTryToReduce(std::max((factor / 100) + 1, 1000))) {
continue;
@@ -1001,6 +994,43 @@ struct Reducer
}
}
+ // Try to empty out the bodies of some functions.
+ bool tryToEmptyFunctions(std::vector<Name> names) {
+ std::vector<Expression*> oldBodies;
+ size_t actuallyEmptied = 0;
+ for (auto name : names) {
+ auto* func = module->getFunction(name);
+ auto* oldBody = func->body;
+ oldBodies.push_back(oldBody);
+ // Nothing to do for imported functions (body is nullptr) or for bodies
+ // that have already been as reduced as we can make them.
+ if (func->imported() || oldBody->is<Unreachable>() ||
+ oldBody->is<Nop>()) {
+ continue;
+ }
+ actuallyEmptied++;
+ bool useUnreachable = func->getResults() != Type::none;
+ if (useUnreachable) {
+ func->body = builder->makeUnreachable();
+ } else {
+ func->body = builder->makeNop();
+ }
+ }
+ if (actuallyEmptied > 0 && writeAndTestReduction()) {
+ std::cerr << "| emptied " << actuallyEmptied << " / "
+ << names.size() << " functions\n";
+ return true;
+ } else {
+ // Restore the bodies.
+ for (size_t i = 0; i < names.size(); i++) {
+ module->getFunction(names[i])->body = oldBodies[i];
+ }
+ return false;
+ }
+ }
+
+ // Try to actually remove functions. If they are somehow referred to, we will
+ // get a validation error and undo it.
bool tryToRemoveFunctions(std::vector<Name> names) {
for (auto name : names) {
module->removeFunction(name);