blob: f4beff1a8b8c2423f9b2371d7c719ea585e7bc60 [file] [log] [blame] [edit]
/*
* Copyright 2016 WebAssembly Community Group participants
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
//
// Pushes code "forward" as much as possible, potentially into
// a location behind a condition, where it might not always execute.
//
#include <wasm.h>
#include <pass.h>
#include <ast_utils.h>
#include <wasm-builder.h>
namespace wasm {
//
// Analyzers some useful local properties: # of sets and gets, and SFA.
//
// Single First Assignment (SFA) form: the local has a single set_local, is
// not a parameter, and has no get_locals before the set_local in postorder.
// This is a much weaker property than SSA, obviously, but together with
// our implicit dominance properties in the structured AST is quite useful.
//
struct LocalAnalyzer : public PostWalker<LocalAnalyzer, Visitor<LocalAnalyzer>> {
std::vector<bool> sfa;
std::vector<Index> numSets;
std::vector<Index> numGets;
void analyze(Function* func) {
auto num = func->getNumLocals();
numSets.resize(num);
std::fill(numSets.begin(), numSets.end(), 0);
numGets.resize(num);
std::fill(numGets.begin(), numGets.end(), 0);
sfa.resize(num);
std::fill(sfa.begin(), sfa.begin() + func->getNumParams(), false);
std::fill(sfa.begin() + func->getNumParams(), sfa.end(), true);
walk(func->body);
for (Index i = 0; i < num; i++) {
if (numSets[i] == 0) sfa[i] = false;
}
}
bool isSFA(Index i) {
return sfa[i];
}
Index getNumGets(Index i) {
return numGets[i];
}
void visitGetLocal(GetLocal *curr) {
if (numSets[curr->index] == 0) {
sfa[curr->index] = false;
}
numGets[curr->index]++;
}
void visitSetLocal(SetLocal *curr) {
numSets[curr->index]++;
if (numSets[curr->index] > 1) {
sfa[curr->index] = false;
}
}
};
// Implement core optimization logic in a struct, used and then discarded entirely
// for each block
class Pusher {
ExpressionList& list;
LocalAnalyzer& analyzer;
std::vector<Index>& numGetsSoFar;
public:
Pusher(Block* block, LocalAnalyzer& analyzer, std::vector<Index>& numGetsSoFar) : list(block->list), analyzer(analyzer), numGetsSoFar(numGetsSoFar) {
// Find an optimization segment: from the first pushable thing, to the first
// point past which we want to push. We then push in that range before
// continuing forward.
Index relevant = list.size() - 1; // we never need to push past a final element, as
// we couldn't be used after it.
Index nothing = -1;
Index i = 0;
Index firstPushable = nothing;
while (i < relevant) {
if (firstPushable == nothing && isPushable(list[i])) {
firstPushable = i;
i++;
continue;
}
if (firstPushable != nothing && isPushPoint(list[i])) {
// optimize this segment, and proceed from where it tells us
i = optimizeSegment(firstPushable, i);
firstPushable = nothing;
continue;
}
i++;
}
}
private:
SetLocal* isPushable(Expression* curr) {
auto* set = curr->dynCast<SetLocal>();
if (!set) return nullptr;
auto index = set->index;
// to be pushable, this must be SFA and the right # of gets,
// but also have no side effects, as it may not execute if pushed.
if (analyzer.isSFA(index) &&
numGetsSoFar[index] == analyzer.getNumGets(index) &&
!EffectAnalyzer(set->value).hasSideEffects()) {
return set;
}
return nullptr;
}
// Push past conditional control flow.
// TODO: push into ifs as well
bool isPushPoint(Expression* curr) {
// look through drops
if (auto* drop = curr->dynCast<Drop>()) {
curr = drop->value;
}
if (curr->is<If>()) return true;
if (auto* br = curr->dynCast<Break>()) {
return !!br->condition;
}
return false;
}
Index optimizeSegment(Index firstPushable, Index pushPoint) {
// The interesting part. Starting at firstPushable, try to push
// code past pushPoint. We start at the end since we are pushing
// forward, that way we can push later things out of the way
// of earlier ones. Once we know all we can push, we push it all
// in one pass, keeping the order of the pushables intact.
assert(firstPushable != Index(-1) && pushPoint != Index(-1) && firstPushable < pushPoint);
EffectAnalyzer cumulativeEffects; // everything that matters if you want
// to be pushed past the pushPoint
cumulativeEffects.analyze(list[pushPoint]);
cumulativeEffects.branches = false; // it is ok to ignore the branching here,
// that is the crucial point of this opt
std::vector<SetLocal*> toPush;
Index i = pushPoint - 1;
while (1) {
auto* pushable = isPushable(list[i]);
if (pushable) {
auto iter = pushableEffects.find(pushable);
if (iter == pushableEffects.end()) {
pushableEffects.emplace(pushable, pushable);
}
auto& effects = pushableEffects[pushable];
if (cumulativeEffects.invalidates(effects)) {
// we can't push this, so further pushables must pass it
cumulativeEffects.mergeIn(effects);
} else {
// we can push this, great!
toPush.push_back(pushable);
}
if (i == firstPushable) {
// no point in looking further
break;
}
} else {
// something that can't be pushed, so it might block further pushing
cumulativeEffects.analyze(list[i]);
}
assert(i > 0);
i--;
}
if (toPush.size() == 0) {
// nothing to do, can only continue after the push point
return pushPoint + 1;
}
// we have work to do!
Index total = toPush.size();
Index last = total - 1;
Index skip = 0;
for (Index i = firstPushable; i <= pushPoint; i++) {
// we see the first elements at the end of toPush
if (skip < total && list[i] == toPush[last - skip]) {
// this is one of our elements to push, skip it
skip++;
} else {
if (skip) {
list[i - skip] = list[i];
}
}
}
assert(skip == total);
// write out the skipped elements
for (Index i = 0; i < total; i++) {
list[pushPoint - i] = toPush[i];
}
// proceed right after the push point, we may push the pushed elements again
return pushPoint - total + 1;
}
// Pushables may need to be scanned more than once, so cache their effects.
std::unordered_map<SetLocal*, EffectAnalyzer> pushableEffects;
};
struct CodePushing : public WalkerPass<PostWalker<CodePushing, Visitor<CodePushing>>> {
bool isFunctionParallel() override { return true; }
Pass* create() override { return new CodePushing; }
LocalAnalyzer analyzer;
// gets seen so far in the main traversal
std::vector<Index> numGetsSoFar;
void doWalkFunction(Function* func) {
// pre-scan to find which vars are sfa, and also count their gets&sets
analyzer.analyze(func);
// prepare to walk
numGetsSoFar.resize(func->getNumLocals());
std::fill(numGetsSoFar.begin(), numGetsSoFar.end(), 0);
// walk and optimize
walk(func->body);
}
void visitGetLocal(GetLocal *curr) {
numGetsSoFar[curr->index]++;
}
void visitBlock(Block* curr) {
// Pushing code only makes sense if we are size 3 or above: we need
// one element to push, an element to push it past, and an element to use
// what we pushed.
if (curr->list.size() < 3) return;
// At this point in the postorder traversal we have gone through all our children.
// Therefore any variable whose gets seen so far is equal to the total gets must
// have no further users after this block. And therefore when we see an SFA
// variable defined here, we know it isn't used before it either, and has just this
// one assign. So we can push it forward while we don't hit a non-control-flow
// ordering invalidation issue, since if this isn't a loop, it's fine (we're not
// used outside), and if it is, we hit the assign before any use (as we can't
// push it past a use).
Pusher pusher(curr, analyzer, numGetsSoFar);
}
};
Pass *createCodePushingPass() {
return new CodePushing();
}
} // namespace wasm