| /* | |
| * 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 <ir/effects.h> | |
| #include <ir/manipulation.h> | |
| #include <pass.h> | |
| #include <wasm-builder.h> | |
| #include <wasm.h> | |
| namespace wasm { | |
| // | |
| // Analyzers some useful local properties: # of sets and gets, and SFA. | |
| // | |
| // Single First Assignment (SFA) form: the local has a single local.set, is | |
| // not a parameter, and has no local.gets before the local.set 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> { | |
| std::vector<bool> sfa; | |
| std::vector<Index> numSets; | |
| std::vector<Index> numGets; | |
| void analyze(Function* func) { | |
| auto num = func->getNumLocals(); | |
| numSets.clear(); | |
| numSets.resize(num); | |
| numGets.clear(); | |
| numGets.resize(num); | |
| sfa.clear(); | |
| sfa.resize(num); | |
| 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 visitLocalGet(LocalGet* curr) { | |
| if (numSets[curr->index] == 0) { | |
| sfa[curr->index] = false; | |
| } | |
| numGets[curr->index]++; | |
| } | |
| void visitLocalSet(LocalSet* curr) { | |
| numSets[curr->index]++; | |
| if (numSets[curr->index] > 1) { | |
| sfa[curr->index] = false; | |
| } | |
| } | |
| }; | |
| // Implements core optimization logic. Used and then discarded entirely | |
| // for each block. | |
| class Pusher { | |
| ExpressionList& list; | |
| LocalAnalyzer& analyzer; | |
| std::vector<Index>& numGetsSoFar; | |
| PassOptions& passOptions; | |
| Module& module; | |
| public: | |
| Pusher(Block* block, | |
| LocalAnalyzer& analyzer, | |
| std::vector<Index>& numGetsSoFar, | |
| PassOptions& passOptions, | |
| Module& module) | |
| : list(block->list), analyzer(analyzer), numGetsSoFar(numGetsSoFar), | |
| passOptions(passOptions), module(module) { | |
| // 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. | |
| const Index nothing = -1; | |
| Index i = 0; | |
| Index firstPushable = nothing; | |
| while (i < list.size()) { | |
| 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. First | |
| // optimize things into the if, if possible, which does not move the | |
| // push point. Then move things past the push point (which has the | |
| // relative effect of moving the push point backwards as other things | |
| // move forward). | |
| optimizeIntoIf(firstPushable, i); | |
| // We never need to push past a final element, as we couldn't be used | |
| // after it. | |
| if (i < list.size() - 1) { | |
| i = optimizeSegment(firstPushable, i); | |
| } | |
| firstPushable = nothing; | |
| continue; | |
| } | |
| i++; | |
| } | |
| } | |
| private: | |
| LocalSet* isPushable(Expression* curr) { | |
| auto* set = curr->dynCast<LocalSet>(); | |
| if (!set) { | |
| return nullptr; | |
| } | |
| auto index = set->index; | |
| // To be pushable, this must be SFA and the right # of gets. | |
| // | |
| // It must also not have side effects, as it may no longer execute after it | |
| // is pushed, since it may be behind a condition that ends up false some of | |
| // the time. However, removable side effects are ok here. The general | |
| // problem with removable effects is that we can only remove them, but not | |
| // move them, because of stuff like this: | |
| // | |
| // if (x != 0) foo(1 / x); | |
| // | |
| // If we move 1 / x to execute unconditionally then it may trap, but it | |
| // would be fine to remove it. This pass does not move code to places where | |
| // it might execute more, but *less*: we keep the code behind any conditions | |
| // it was already behind, and potentially put it behind further ones. In | |
| // effect, we "partially remove" the code, making it not execute some of the | |
| // time, which is fine. | |
| if (analyzer.isSFA(index) && | |
| numGetsSoFar[index] == analyzer.getNumGets(index) && | |
| !EffectAnalyzer(passOptions, module, set->value) | |
| .hasUnremovableSideEffects()) { | |
| return set; | |
| } | |
| return nullptr; | |
| } | |
| // Try to 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>() || curr->is<BrOn>()) { | |
| 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); | |
| // everything that matters if you want to be pushed past the pushPoint | |
| EffectAnalyzer cumulativeEffects(passOptions, module); | |
| cumulativeEffects.walk(list[pushPoint]); | |
| // It is ok to ignore branching out of the block here, that is the crucial | |
| // point of this optimization. That is, we are in a situation like this: | |
| // | |
| // { | |
| // x = value; | |
| // if (..) break; | |
| // foo(x); | |
| // } | |
| // | |
| // If the branch is taken, then that's fine, it will jump out of this block | |
| // and reach some outer scope, and in that case we never need x at all | |
| // (since we've proven before that x is not used outside of this block, see | |
| // numGetsSoFar which we use for that). Similarly, control flow could | |
| // transfer away via a return or an exception and that would be ok as well. | |
| cumulativeEffects.ignoreControlFlowTransfers(); | |
| std::vector<LocalSet*> toPush; | |
| Index i = pushPoint - 1; | |
| while (1) { | |
| auto* pushable = isPushable(list[i]); | |
| if (pushable) { | |
| const auto& effects = getPushableEffects(pushable); | |
| if (effects.orderedBefore(cumulativeEffects)) { | |
| // we can't push this, so further pushables must pass it | |
| cumulativeEffects.mergeIn(effects); | |
| } else { | |
| // we can push this, great! | |
| toPush.push_back(pushable); | |
| } | |
| } else { | |
| // something that can't be pushed, so it might block further pushing | |
| cumulativeEffects.walk(list[i]); | |
| } | |
| if (i == firstPushable) { | |
| // no point in looking further | |
| break; | |
| } | |
| 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; | |
| } | |
| // Similar to optimizeSegment, but for the case where the push point is an if, | |
| // and we try to push into the if's arms, doing things like this: | |
| // | |
| // x = op(); | |
| // if (..) { | |
| // .. | |
| // } | |
| // => | |
| // if (..) { | |
| // x = op(); // this moved | |
| // .. | |
| // } | |
| // | |
| // This does not move the push point, so it does not have a return value, | |
| // unlike optimizeSegment. | |
| void optimizeIntoIf(Index firstPushable, Index pushPoint) { | |
| assert(firstPushable != Index(-1) && pushPoint != Index(-1) && | |
| firstPushable < pushPoint); | |
| auto* iff = list[pushPoint]->dynCast<If>(); | |
| if (!iff) { | |
| return; | |
| } | |
| // Everything that matters if you want to be pushed past the pushPoint. This | |
| // begins with the if condition's effects, as we must always push past | |
| // those. Later, we will add to this when we need to. | |
| EffectAnalyzer cumulativeEffects(passOptions, module, iff->condition); | |
| // See optimizeSegment for why we can ignore control flow transfers here. | |
| cumulativeEffects.ignoreControlFlowTransfers(); | |
| // Find the effects of the arms, which will affect what can be pushed. | |
| EffectAnalyzer ifTrueEffects(passOptions, module, iff->ifTrue); | |
| EffectAnalyzer ifFalseEffects(passOptions, module); | |
| if (iff->ifFalse) { | |
| ifFalseEffects.walk(iff->ifFalse); | |
| } | |
| // We need to know which locals are used after the if, as that can determine | |
| // if we can push or not. | |
| EffectAnalyzer postIfEffects(passOptions, module); | |
| for (Index i = pushPoint + 1; i < list.size(); i++) { | |
| postIfEffects.walk(list[i]); | |
| } | |
| // Start at the instruction right before the push point, and go back from | |
| // there: | |
| // | |
| // x = op(); | |
| // y = op(); | |
| // if (..) { | |
| // .. | |
| // } | |
| // | |
| // Here we will try to push y first, and then x. Note that if we push y | |
| // then we can immediately try to push x after it, as it will remain in | |
| // order with x if we do. If we do *not* push y we can still try to push x | |
| // but we must move it past y, which means we need to check for interference | |
| // between them (which we do by adding y's effects to cumulativeEffects). | |
| // | |
| // Decrement at the top of the loop for simplicity, so start with i at one | |
| // past the first thing we can push (which is right before the push point). | |
| Index i = pushPoint; | |
| while (1) { | |
| if (i == firstPushable) { | |
| // We just finished processing the first thing that could be pushed; | |
| // stop. | |
| break; | |
| } | |
| assert(i > 0); | |
| i--; | |
| auto* pushable = isPushable(list[i]); | |
| if (pushable && pushable->type == Type::unreachable) { | |
| // Don't try to push something unreachable. If we did, then we'd need to | |
| // refinalize the block we are moving it from: | |
| // | |
| // (block $unreachable | |
| // (local.set $x (unreachable)) | |
| // (if (..) | |
| // (.. (local.get $x)) | |
| // ) | |
| // | |
| // The block should not be unreachable if the local.set is moved into | |
| // the if arm (the if arm may not execute, so the if itself will not | |
| // change type). It is simpler to avoid this complexity and leave this | |
| // to DCE to simplify first. | |
| // | |
| // (Note that the side effect of trapping will normally prevent us from | |
| // trying to push something unreachable, but in traps-never-happen mode | |
| // we are allowed to ignore that, and so we need this check.) | |
| pushable = nullptr; | |
| } | |
| if (!pushable) { | |
| // Something that is staying where it is, so anything we push later must | |
| // move past it. Note the effects and continue. | |
| cumulativeEffects.walk(list[i]); | |
| continue; | |
| } | |
| auto index = pushable->index; | |
| const auto& effects = getPushableEffects(pushable); | |
| if (effects.orderedBefore(cumulativeEffects)) { | |
| // This can't be moved forward. Add it to the things that are not | |
| // moving. | |
| cumulativeEffects.walk(list[i]); | |
| continue; | |
| } | |
| // We only try to push into an arm if the local is used there. If the | |
| // local is not used in either arm then we'll want to push it past the | |
| // entire if, which is what optimizeSegment handles. | |
| // | |
| // We can push into the if-true arm if the local cannot be used if we go | |
| // through the other arm: | |
| // | |
| // x = op(); | |
| // if (..) { | |
| // // we would like to move "x = op()" to here | |
| // .. | |
| // } else { | |
| // use(x); | |
| // } | |
| // use(x); | |
| // | |
| // Either of those use(x)s would stop us from moving to if-true arm. | |
| // | |
| // One specific case we handle is if there is a use after the if but the | |
| // arm we don't push into is unreachable. In that case we only get to the | |
| // later code after going through the reachable arm, which is ok to push | |
| // into: | |
| // | |
| // x = op(); | |
| // if (..) { | |
| // // We'll push "x = op()" to here. | |
| // use(x); | |
| // } else { | |
| // return; | |
| // } | |
| // use(x); | |
| auto maybePushInto = [&](Expression*& arm, | |
| const Expression* otherArm, | |
| EffectAnalyzer& armEffects, | |
| const EffectAnalyzer& otherArmEffects) { | |
| if (!arm || !armEffects.localsRead.contains(index) || | |
| otherArmEffects.localsRead.contains(index)) { | |
| // No arm, or this arm has no read of the index, or the other arm | |
| // reads the index. | |
| return false; | |
| } | |
| if (postIfEffects.localsRead.contains(index) && | |
| (!otherArm || otherArm->type != Type::unreachable)) { | |
| // The local is read later, which is bad, and there is no unreachable | |
| // in the other arm which as mentioned above is the only thing that | |
| // could have made it work out for us. | |
| return false; | |
| } | |
| // We can do it! Push into one of the if's arms, and put a nop where it | |
| // used to be. | |
| Builder builder(module); | |
| auto* block = builder.blockify(arm); | |
| arm = block; | |
| // TODO: this is quadratic in the number of pushed things | |
| ExpressionManipulator::spliceIntoBlock(block, 0, pushable); | |
| list[i] = builder.makeNop(); | |
| // The code we pushed adds to the effects in that arm. | |
| armEffects.walk(pushable); | |
| // TODO: After pushing we could recurse and run both this function and | |
| // optimizeSegment in that location. For now, leave that to later | |
| // cycles of the optimizer, as this case seems fairly rare. | |
| return true; | |
| }; | |
| if (!maybePushInto( | |
| iff->ifTrue, iff->ifFalse, ifTrueEffects, ifFalseEffects) && | |
| !maybePushInto( | |
| iff->ifFalse, iff->ifTrue, ifFalseEffects, ifTrueEffects)) { | |
| // We didn't push this anywhere, so further pushables must pass it. | |
| cumulativeEffects.mergeIn(effects); | |
| } | |
| } | |
| } | |
| const EffectAnalyzer& getPushableEffects(LocalSet* pushable) { | |
| auto iter = pushableEffects.find(pushable); | |
| if (iter == pushableEffects.end()) { | |
| iter = | |
| pushableEffects.try_emplace(pushable, passOptions, module, pushable) | |
| .first; | |
| } | |
| return iter->second; | |
| } | |
| // Pushables may need to be scanned more than once, so cache their effects. | |
| std::unordered_map<LocalSet*, EffectAnalyzer> pushableEffects; | |
| }; | |
| struct CodePushing : public WalkerPass<PostWalker<CodePushing>> { | |
| bool isFunctionParallel() override { return true; } | |
| std::unique_ptr<Pass> create() override { | |
| return std::make_unique<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.clear(); | |
| numGetsSoFar.resize(func->getNumLocals()); | |
| // walk and optimize | |
| walk(func->body); | |
| } | |
| void visitLocalGet(LocalGet* curr) { numGetsSoFar[curr->index]++; } | |
| void visitBlock(Block* curr) { | |
| // Pushing code only makes sense if we are size 2 or above: we need one | |
| // element to push and an element to push it into, at minimum. | |
| if (curr->list.size() < 2) { | |
| 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, getPassOptions(), *getModule()); | |
| } | |
| }; | |
| Pass* createCodePushingPass() { return new CodePushing(); } | |
| } // namespace wasm |