| /* | |
| * Copyright 2023 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. | |
| */ | |
| // | |
| // Optimize away trivial tuples. When values are bundled together in a tuple, we | |
| // are limited in how we can optimize then in the various local-related passes, | |
| // like this: | |
| // | |
| // (local.set $tuple | |
| // (tuple.make 3 (A) (B) (C))) | |
| // (use | |
| // (tuple.extract 3 0 | |
| // (local.get $tuple))) | |
| // | |
| // If there are no other uses, then we just need one of the three elements. By | |
| // lowing them to three separate locals, other passes can remove the other two. | |
| // | |
| // Specifically, this pass seeks out tuple locals that have these properties: | |
| // | |
| // * They are always written either a tuple.make or another tuple local with | |
| // these properties. | |
| // * They are always used either in tuple.extract or they are copied to another | |
| // tuple local with these properties. | |
| // | |
| // The set of those tuple locals can be easily optimized into individual locals, | |
| // as the tuple does not "escape" into, say, a return value. | |
| // | |
| // TODO: Blocks etc. might be handled here, but it's not clear if we want to: | |
| // there are situations where multivalue leads to smaller code using | |
| // those constructs. Atm this pass should only remove things that are | |
| // definitely worth lowering. | |
| // | |
| #include <ir/effects.h> | |
| #include <pass.h> | |
| #include <support/unique_deferring_queue.h> | |
| #include <wasm-builder.h> | |
| #include <wasm.h> | |
| namespace wasm { | |
| namespace { | |
| // Helper class to analyze interferences between tuple.make operands and their | |
| // target locals. When lowering a tuple local.set to individual local.sets: | |
| // | |
| // (local.set $t (tuple.make op0 op1 ... opN-1)) | |
| // | |
| // into: | |
| // | |
| // (local.set $t0 op0) | |
| // (local.set $t1 op1) | |
| // ... | |
| // | |
| // an operand op_i cannot be written directly to target local $ti if doing so | |
| // interferes with the evaluation of any subsequent operand op_j (j > i). An | |
| // interference occurs if: | |
| // 1. op_j reads $ti (op_j would see the new value of $ti instead of the old | |
| // value). | |
| // 2. op_j writes $ti (op_j's write would be overwritten by op_i's value later | |
| // in the original code, but would overwrite op_i's value here). | |
| // 3. op_j transfers control flow (e.g. branches to an enclosing block or | |
| // throws; in the original code, no target locals are written if control | |
| // flow transfers out to an enclosing scope in the same function). | |
| // | |
| // Any operand that interferes must be written to a scratch local first and then | |
| // copied to its target local after all operands have been evaluated. | |
| class TupleInterferenceFinder { | |
| std::vector<bool> interfering; | |
| public: | |
| TupleInterferenceFinder(const ExpressionList& operands, | |
| Index targetBase, | |
| const PassOptions& passOptions, | |
| const Module& wasm) | |
| : interfering(operands.size(), false) { | |
| Index numOperands = operands.size(); | |
| assert(numOperands > 1); | |
| std::unordered_set<Index> subsequentReads; | |
| std::unordered_set<Index> subsequentWrites; | |
| bool subsequentTransfersControlFlow = false; | |
| for (Index i = numOperands; i > 0; i--) { | |
| Index opIndex = i - 1; | |
| Index targetLocal = targetBase + opIndex; | |
| if (subsequentTransfersControlFlow || | |
| subsequentReads.contains(targetLocal) || | |
| subsequentWrites.contains(targetLocal)) { | |
| interfering[opIndex] = true; | |
| } | |
| EffectAnalyzer effects(passOptions, wasm, operands[opIndex]); | |
| subsequentReads.insert(effects.localsRead.begin(), | |
| effects.localsRead.end()); | |
| subsequentWrites.insert(effects.localsWritten.begin(), | |
| effects.localsWritten.end()); | |
| if (effects.transfersControlFlow()) { | |
| subsequentTransfersControlFlow = true; | |
| } | |
| } | |
| } | |
| bool interferes(Index i) const { return interfering[i]; } | |
| }; | |
| } // anonymous namespace | |
| struct TupleOptimization : public WalkerPass<PostWalker<TupleOptimization>> { | |
| bool isFunctionParallel() override { return true; } | |
| std::unique_ptr<Pass> create() override { | |
| return std::make_unique<TupleOptimization>(); | |
| } | |
| // Track the number of uses for each tuple local. We consider a use as a | |
| // local.get, a set, or a tee. A tee counts as two uses (since it both sets | |
| // and gets, and so we must see that it is both used and uses properly). | |
| std::vector<Index> uses; | |
| // Tracks which tuple local uses are valid, that is, follow the properties | |
| // above. If we have more uses than valid uses then we must have an invalid | |
| // one, and the local cannot be optimized. | |
| std::vector<Index> validUses; | |
| // When one tuple local copies the value of another, we need to track the | |
| // index that was copied, as if the source ends up bad then the target is bad | |
| // as well. | |
| // | |
| // This is a symmetrical map, that is, we consider copies to work both ways: | |
| // | |
| // x \in copiedIndexed[y] <==> y \in copiedIndexed[x] | |
| // | |
| std::vector<std::unordered_set<Index>> copiedIndexes; | |
| void doWalkFunction(Function* func) { | |
| // If tuples are not enabled, or there are no tuple locals, then there is no | |
| // work to do. | |
| if (!getModule()->features.hasMultivalue()) { | |
| return; | |
| } | |
| bool hasTuple = false; | |
| for (auto var : func->vars) { | |
| if (var.isTuple()) { | |
| hasTuple = true; | |
| break; | |
| } | |
| } | |
| if (!hasTuple) { | |
| return; | |
| } | |
| // Prepare global data structures before we collect info. | |
| auto numLocals = func->getNumLocals(); | |
| uses.resize(numLocals); | |
| validUses.resize(numLocals); | |
| copiedIndexes.resize(numLocals); | |
| // Walk the code to collect info. | |
| Super::doWalkFunction(func); | |
| // Analyze and optimize. | |
| optimize(func); | |
| } | |
| void visitLocalGet(LocalGet* curr) { | |
| if (curr->type.isTuple()) { | |
| uses[curr->index]++; | |
| } | |
| } | |
| void visitLocalSet(LocalSet* curr) { | |
| if (getFunction()->getLocalType(curr->index).isTuple()) { | |
| // See comment above about tees (we consider their set and get each a | |
| // separate use). | |
| uses[curr->index] += curr->isTee() ? 2 : 1; | |
| auto* value = curr->value; | |
| // We need the input to the local to be another such local (from a tee, or | |
| // a get), or a tuple.make. | |
| if (auto* tee = value->dynCast<LocalSet>()) { | |
| assert(tee->isTee()); | |
| // We don't want to count anything as valid if the inner tee is | |
| // unreachable. In that case the outer tee is also unreachable, of | |
| // course, and in fact they might not even have the same tuple type or | |
| // the inner one might not even be a tuple (since we are in unreachable | |
| // code, that is possible). We could try to optimize unreachable tees in | |
| // some cases, but it's simpler to let DCE simplify the code first. | |
| if (tee->type != Type::unreachable) { | |
| validUses[tee->index]++; | |
| validUses[curr->index]++; | |
| copiedIndexes[tee->index].insert(curr->index); | |
| copiedIndexes[curr->index].insert(tee->index); | |
| } | |
| } else if (auto* get = value->dynCast<LocalGet>()) { | |
| validUses[get->index]++; | |
| validUses[curr->index]++; | |
| copiedIndexes[get->index].insert(curr->index); | |
| copiedIndexes[curr->index].insert(get->index); | |
| } else if (value->is<TupleMake>()) { | |
| validUses[curr->index]++; | |
| } | |
| } | |
| } | |
| void visitTupleExtract(TupleExtract* curr) { | |
| // We need the input to be a local, either from a tee or a get. | |
| if (auto* tee = curr->tuple->dynCast<LocalSet>()) { | |
| // The tee might not be of a tuple local if it is unreachable. | |
| if (getFunction()->getLocalType(tee->index).isTuple()) { | |
| validUses[tee->index]++; | |
| } | |
| } else if (auto* get = curr->tuple->dynCast<LocalGet>()) { | |
| validUses[get->index]++; | |
| } | |
| } | |
| void optimize(Function* func) { | |
| auto numLocals = func->getNumLocals(); | |
| // Find the set of bad indexes. We add each such candidate to a worklist | |
| // that we will then flow to find all those corrupted. | |
| std::vector<bool> bad(numLocals); | |
| UniqueDeferredQueue<Index> work; | |
| for (Index i = 0; i < uses.size(); i++) { | |
| assert(validUses[i] <= uses[i]); | |
| if (uses[i] > 0 && validUses[i] < uses[i]) { | |
| // This is a bad tuple. | |
| work.push(i); | |
| } | |
| } | |
| // Flow badness forward. | |
| while (!work.empty()) { | |
| auto i = work.pop(); | |
| if (bad[i]) { | |
| continue; | |
| } | |
| bad[i] = true; | |
| for (auto target : copiedIndexes[i]) { | |
| work.push(target); | |
| } | |
| } | |
| // Good indexes we can optimize are tuple locals with uses that are not bad. | |
| std::vector<bool> good(numLocals); | |
| bool hasGood = false; | |
| for (Index i = 0; i < uses.size(); i++) { | |
| if (uses[i] > 0 && !bad[i]) { | |
| good[i] = true; | |
| hasGood = true; | |
| } | |
| } | |
| if (!hasGood) { | |
| return; | |
| } | |
| // We found things to optimize! Create new non-tuple locals for their | |
| // contents, and then rewrite the code to use those according to the | |
| // mapping from tuple locals to normal ones. The mapping maps a tuple local | |
| // to the base index used for its contents: an index and several others | |
| // right after it, depending on the tuple size. | |
| std::unordered_map<Index, Index> tupleToNewBaseMap; | |
| for (Index i = 0; i < good.size(); i++) { | |
| if (!good[i]) { | |
| continue; | |
| } | |
| auto newBase = func->getNumLocals(); | |
| tupleToNewBaseMap[i] = newBase; | |
| Index lastNewIndex = 0; | |
| for (auto t : func->getLocalType(i)) { | |
| Index newIndex = Builder::addVar(func, t); | |
| if (lastNewIndex == 0) { | |
| // This is the first new local we added (0 is an impossible value, | |
| // since tuple locals exist, hence index 0 was already taken), so it | |
| // must be equal to the base. | |
| assert(newIndex == newBase); | |
| } else { | |
| // This must be right after the former. | |
| assert(newIndex == lastNewIndex + 1); | |
| } | |
| lastNewIndex = newIndex; | |
| } | |
| } | |
| MapApplier mapApplier(tupleToNewBaseMap, getPassOptions()); | |
| mapApplier.walkFunctionInModule(func, getModule()); | |
| } | |
| struct MapApplier : public PostWalker<MapApplier> { | |
| std::unordered_map<Index, Index>& tupleToNewBaseMap; | |
| const PassOptions& passOptions; | |
| MapApplier(std::unordered_map<Index, Index>& tupleToNewBaseMap, | |
| const PassOptions& passOptions) | |
| : tupleToNewBaseMap(tupleToNewBaseMap), passOptions(passOptions) {} | |
| // Gets the new base index if there is one, or 0 if not (0 is an impossible | |
| // value for a new index, as local index 0 was taken before, as tuple | |
| // locals existed). | |
| Index getNewBaseIndex(Index i) { | |
| auto iter = tupleToNewBaseMap.find(i); | |
| if (iter == tupleToNewBaseMap.end()) { | |
| return 0; | |
| } | |
| return iter->second; | |
| } | |
| // Given a local.get or local.set, return the new base index for the local | |
| // index used there. Returns 0 (an impossible value, see above) otherwise. | |
| Index getSetOrGetBaseIndex(Expression* setOrGet) { | |
| Index index; | |
| if (auto* set = setOrGet->dynCast<LocalSet>()) { | |
| index = set->index; | |
| } else if (auto* get = setOrGet->dynCast<LocalGet>()) { | |
| index = get->index; | |
| } else { | |
| return 0; | |
| } | |
| return getNewBaseIndex(index); | |
| } | |
| // Replacing a local.tee requires some care, since we might have | |
| // | |
| // (local.set | |
| // (local.tee | |
| // .. | |
| // | |
| // We replace the local.tee with a block of sets of the new non-tuple | |
| // locals, and the outer set must then (1) keep those around and also (2) | |
| // identify the local that was tee'd, so we know what to get (which has been | |
| // replaced by the block). To make that simple keep a map of the things that | |
| // replaced tees. | |
| std::unordered_map<Expression*, LocalSet*> replacedTees; | |
| void visitLocalSet(LocalSet* curr) { | |
| auto replace = [&](Expression* replacement) { | |
| if (curr->isTee()) { | |
| replacedTees[replacement] = curr; | |
| } | |
| replaceCurrent(replacement); | |
| }; | |
| if (auto targetBase = getNewBaseIndex(curr->index)) { | |
| Builder builder(*getModule()); | |
| auto type = getFunction()->getLocalType(curr->index); | |
| auto* value = curr->value; | |
| if (auto* make = value->dynCast<TupleMake>()) { | |
| // If writing an operand directly to its target local would interfere | |
| // with any subsequent operand (e.g. in a tuple swap), write it to a | |
| // temporary local first and copy it at the end. | |
| Index numOperands = type.size(); | |
| TupleInterferenceFinder interferences( | |
| make->operands, targetBase, passOptions, *getModule()); | |
| std::vector<Index> tempIndexes(numOperands); | |
| for (Index i = 0; i < numOperands; i++) { | |
| if (interferences.interferes(i)) { | |
| tempIndexes[i] = Builder::addVar(getFunction(), type[i]); | |
| } | |
| } | |
| std::vector<Expression*> sets; | |
| for (Index i = 0; i < numOperands; i++) { | |
| Index dest = | |
| interferences.interferes(i) ? tempIndexes[i] : targetBase + i; | |
| sets.push_back(builder.makeLocalSet(dest, make->operands[i])); | |
| } | |
| for (Index i = 0; i < numOperands; i++) { | |
| if (interferences.interferes(i)) { | |
| sets.push_back(builder.makeLocalSet( | |
| targetBase + i, builder.makeLocalGet(tempIndexes[i], type[i]))); | |
| } | |
| } | |
| replace(builder.makeBlock(sets)); | |
| return; | |
| } | |
| std::vector<Expression*> contents; | |
| auto iter = replacedTees.find(value); | |
| if (iter != replacedTees.end()) { | |
| // The input to us was a tee that has been replaced. The actual value | |
| // we read from (the tee) can be found in replacedTees. Also, we | |
| // need to keep around the replacement of the tee. | |
| contents.push_back(value); | |
| value = iter->second; | |
| } | |
| // This is a copy of a tuple local into another. Copy all the fields | |
| // between them. | |
| Index sourceBase = getSetOrGetBaseIndex(value); | |
| // The target is being optimized, so the source must be as well, or else | |
| // we were confused earlier and the target should not be. | |
| assert(sourceBase); | |
| // The source and target may have different element types due to | |
| // subtyping (but their sizes must be equal). | |
| auto sourceType = value->type; | |
| assert(sourceType.size() == type.size()); | |
| for (Index i = 0; i < type.size(); i++) { | |
| auto* get = builder.makeLocalGet(sourceBase + i, sourceType[i]); | |
| contents.push_back(builder.makeLocalSet(targetBase + i, get)); | |
| } | |
| replace(builder.makeBlock(contents)); | |
| } | |
| } | |
| void visitTupleExtract(TupleExtract* curr) { | |
| auto* value = curr->tuple; | |
| Expression* extraContents = nullptr; | |
| auto iter = replacedTees.find(value); | |
| if (iter != replacedTees.end()) { | |
| // The input to us was a tee that has been replaced. Handle it as in | |
| // visitLocalSet. | |
| extraContents = value; | |
| value = iter->second; | |
| } | |
| auto type = value->type; | |
| if (type == Type::unreachable) { | |
| return; | |
| } | |
| Index sourceBase = getSetOrGetBaseIndex(value); | |
| if (!sourceBase) { | |
| return; | |
| } | |
| Builder builder(*getModule()); | |
| auto i = curr->index; | |
| auto* get = builder.makeLocalGet(sourceBase + i, type[i]); | |
| if (extraContents) { | |
| replaceCurrent(builder.makeSequence(extraContents, get)); | |
| } else { | |
| replaceCurrent(get); | |
| } | |
| } | |
| }; | |
| }; | |
| Pass* createTupleOptimizationPass() { return new TupleOptimization(); } | |
| } // namespace wasm |