| /* | |
| * Copyright 2025 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. | |
| */ | |
| // Perform dead argument elimination based on a smallest fixed point analysis of | |
| // used parameters. Traverse the module once to collect call graph information, | |
| // used parameters, and "forwarded" parameters that are only used by being | |
| // forwarded on to other function calls. Parameters are forwarded if their | |
| // local.gets are consumed as parameters to function calls or if they are | |
| // consumed by other side-effect-free instructions that are transitively | |
| // forwarded to function calls. These forwarded parameters (and their | |
| // intermediate users) can still be optimized out as long as they are unused in | |
| // the callees they are forwarded to. Since we perform a fixed point analysis, | |
| // cycles of forwarded parameters can still be removed. | |
| // | |
| // After finding used parameters, traverse the module once more to remove | |
| // unused parameters and arguments. Finally, if we are able to optimize indirect | |
| // calls and referenced functions, traverse the module one last time to globally | |
| // update referenced function types. This may require first giving unreferenced | |
| // functions replacement types to make sure they are not incorrectly updated by | |
| // the global type rewriting. | |
| // | |
| // As a POC, only do the backward analysis to find unused parameters. To match | |
| // and exceed the power of DAE, we will need to extend this backward analysis to | |
| // find unused results as well, and also add a forward analysis that propagates | |
| // constants and types through parameters and results. | |
| #include <algorithm> | |
| #include <memory> | |
| #include <unordered_map> | |
| #include <vector> | |
| #include "analysis/lattices/bool.h" | |
| #include "ir/effects.h" | |
| #include "ir/eh-utils.h" | |
| #include "ir/intrinsics.h" | |
| #include "ir/label-utils.h" | |
| #include "ir/local-graph.h" | |
| #include "ir/module-utils.h" | |
| #include "ir/type-updating.h" | |
| #include "pass.h" | |
| #include "support/index.h" | |
| #include "support/mixed_arena.h" | |
| #include "support/utilities.h" | |
| #include "wasm-builder.h" | |
| #include "wasm-traversal.h" | |
| #include "wasm-type-shape.h" | |
| #include "wasm-type.h" | |
| #include "wasm.h" | |
| #ifndef TIME_DAE | |
| #define TIME_DAE 0 | |
| #endif | |
| #ifndef DAE_STATS | |
| #define DAE_STATS 0 | |
| #endif | |
| #if TIME_DAE || DAE_STATS | |
| #include <iostream> | |
| #include "support/insert_ordered.h" | |
| #include "support/strongly_connected_components.h" | |
| #include "support/timing.h" | |
| #endif // TIME_DAE || DAE_STATS | |
| // TODO: Treat call_indirects more precisely than call_refs by taking the target | |
| // table into account. | |
| // TODO: Analyze stack switching instructions to remove their unused parameters. | |
| namespace wasm { | |
| namespace { | |
| #if TIME_DAE | |
| #define TIME(...) __VA_ARGS__ | |
| #else | |
| #define TIME(...) | |
| #endif // TIME_DAE | |
| // Find the non-basic root of the subtyping hierarchy for a given HeapType. | |
| HeapType getRootType(HeapType type) { | |
| while (true) { | |
| if (auto super = type.getDeclaredSuperType()) { | |
| type = *super; | |
| continue; | |
| } | |
| break; | |
| } | |
| return type; | |
| } | |
| // Analysis lattice: top/true = used, bot/false = unused. | |
| using Used = analysis::Bool; | |
| // Analysis results for each parameter of a function. | |
| using Params = std::vector<Used::Element>; | |
| // Function index and parameter index. | |
| using FuncParamLoc = std::pair<Index, Index>; | |
| // Function index identifying the function's result (we treat result tuples as a | |
| // single value, so no index into the results is necessary). | |
| using FuncResultLoc = Index; | |
| // Function type and parameter index. | |
| using TypeParamLoc = std::pair<HeapType, Index>; | |
| // Function type identifying the function type's result (we treat result tuples | |
| // as a single value, so no index into the results is necessary). | |
| using TypeResultLoc = HeapType; | |
| using Location = | |
| std::variant<FuncParamLoc, FuncResultLoc, TypeParamLoc, TypeResultLoc>; | |
| // A set of (source, destination) index pairs for parameters of a caller | |
| // function being forwarded as arguments to a callee function. | |
| using ForwardedParamSet = std::unordered_set<std::pair<Index, Index>>; | |
| // Map param indices (in the outer vector) to lists of locations. | |
| // TODO: Experiment with using a set in place of the inner vector. | |
| using ParamLocations = std::vector<std::vector<Location>>; | |
| using TypeMap = GlobalTypeRewriter::TypeMap; | |
| // Analysis results and call graph information for a single function. | |
| // This tracks how parameters are used within the function and how they | |
| // are forwarded to other functions via direct and indirect calls. | |
| struct FunctionInfo { | |
| // Analysis results. For each parameter, whether it is used. | |
| Params paramUsages; | |
| // Analysis result for the function result. It will remain bot if there is no | |
| // result. | |
| Used::Element resultUsage; | |
| // Map direct callee function names to the source locations forwarded to their | |
| // parameters. | |
| std::unordered_map<Name, ParamLocations> forwardedToDirectParams; | |
| // Map the root supertypes of indirect callee types to the source locations | |
| // forwarded to their parameters. | |
| std::unordered_map<HeapType, ParamLocations> forwardedToIndirectParams; | |
| // Locations forwarded to this function's result. These locations will become | |
| // used if the result turns out to be used. | |
| std::vector<Location> resultSources; | |
| // For each parameter of this function, the list of locations that will become | |
| // used if the parameter turns out to be used. Computed by reversing the | |
| // forwardedToDirectParams graph. | |
| ParamLocations paramSources; | |
| // Locations used in an observable way by the code in this function. | |
| // Propagation of usage will begin at these locations. | |
| // TODO: Experiment with making this a set. | |
| std::vector<Location> usedLocations; | |
| // The gets that may read from parameters. These are the gets that might be | |
| // optimized out if their results are unused or forwarded to another function | |
| // where they will be unused. | |
| std::unordered_set<LocalGet*> paramGets; | |
| // We do not yet analyze parameter or result usage in stack switching | |
| // instructions. Collect the used continuation types so we can be sure not to | |
| // modify their associated function types. | |
| // TODO: Analyze stack switching. | |
| std::unordered_set<HeapType> contTypes; | |
| // Whether we need to additionally propagate param usage to and result usage | |
| // from indirect callers of this function's type. Atomic because it can be set | |
| // when visiting other functions in parallel. | |
| std::atomic<bool> referenced = false; | |
| // We cannot yet fully analyze and optimize call.without.effects, which would | |
| // require creating new imports for new signatures, etc. Functions that are | |
| // called via these intrinsics will not be optimized. | |
| // TODO: Fix this. | |
| std::atomic<bool> usedInIntrinsic = false; | |
| // Unreferenced functions can be optimized separately from referenced | |
| // functions with the same type. For unreferenced functions in that situation, | |
| // this is the new type that should be applied before global type rewriting to | |
| // prevent the function from getting the wrong optimizations. | |
| std::optional<HeapType> replacementType; | |
| // Functions that this function directly tail-calls. | |
| std::vector<Index> tailCallees; | |
| // Root function types that this function indirectly tail-calls. | |
| std::vector<HeapType> tailCalleeTypes; | |
| }; | |
| // Analysis results and call graph information for a tree of related function | |
| // types. Every type in the tree must have matching used and unused parameters, | |
| // so we can track information per-tree instead of per-type. | |
| struct RootFuncTypeInfo { | |
| // For each parameter in the type, whether it is used. | |
| Params paramUsages; | |
| // Analysis result for the function result. | |
| Used::Element resultUsage; | |
| // The list of referenced functions with types in this tree. When a parameter | |
| // in this type tree is used, the parameter becomes used in these functions | |
| // and vice versa. | |
| std::vector<Index> referencedFuncs; | |
| // For each parameter in this root function type, the list of locations that | |
| // become used when the parameter in this root function type becomes used. | |
| // Computed by reversing indirectForwardedParams from the function infos for | |
| // functions with this root type. | |
| ParamLocations paramSources; | |
| // Tail-callers of this type tree. If this type tree's result is used, | |
| // these callers' results must also be used. Normally, result usage only | |
| // flows from types to their referenced functions, but tail calls require | |
| // this reverse constraint. | |
| std::vector<Location> resultSources; | |
| RootFuncTypeInfo(Used& used, HeapType type) | |
| : paramUsages(type.getSignature().params.size(), used.getBottom()), | |
| resultUsage(used.getBottom()), | |
| paramSources(type.getSignature().params.size()) {} | |
| }; | |
| struct DAE2 : public Pass { | |
| // Analysis lattice. | |
| Used used; | |
| Module* wasm = nullptr; | |
| // Map function name to index. | |
| std::unordered_map<Name, Index> funcIndices; | |
| // Intermediate and final analysis results by function index. | |
| std::vector<FunctionInfo> funcInfos; | |
| // Intermediate and final analysis results for each function type tree, keyed | |
| // by root type in the tree. | |
| std::unordered_map<HeapType, RootFuncTypeInfo> typeTreeInfos; | |
| RootFuncTypeInfo& getTypeTreeInfo(HeapType rootType) { | |
| return typeTreeInfos.try_emplace(rootType, used, rootType).first->second; | |
| } | |
| // In general referenced functions may escape and be called externally in an | |
| // open world, so we require a closed world to optimize referenced functions. | |
| // Further, without GC we cannot differentiate the types of unreferenced and | |
| // referenced functions before global type rewriting, so we cannot optimize | |
| // them separately. Do not constrain the optimization of unreferenced | |
| // functions by optimizing referenced functions in that case. | |
| // TODO: Find a way to optimize referenced functions without GC enabled as | |
| // long as traps never happen so call_indirect cannot distinguish separate | |
| // types. | |
| bool optimizeReferencedFuncs = false; | |
| // Cache the public heap types to avoid gathering them more than once. | |
| std::vector<HeapType> publicHeapTypes; | |
| void run(Module* wasm) override { | |
| this->wasm = wasm; | |
| for (auto& func : wasm->functions) { | |
| funcIndices.insert({func->name, funcIndices.size()}); | |
| } | |
| optimizeReferencedFuncs = | |
| getPassOptions().worldMode == WorldMode::Closed && wasm->features.hasGC(); | |
| TIME(Timer timer); | |
| analyzeModule(); | |
| TIME(std::cerr << "analysis: " << timer.lastElapsed() << "\n"); | |
| prepareReverseGraph(); | |
| TIME(std::cerr << "prepare: " << timer.lastElapsed() << "\n"); | |
| computeFixedPoint(); | |
| TIME(std::cerr << "fixed point: " << timer.lastElapsed() << "\n"); | |
| #if DAE_STATS | |
| collectStats(); | |
| TIME(std::cerr << "stats: " << timer.lastElapsed() << "\n"); | |
| #endif // DAE_STATS | |
| optimize(); | |
| TIME(auto [last, total] = timer.elapsed()); | |
| TIME(std::cerr << "optimize: " << last << "\n"); | |
| TIME(std::cerr << "total: " << total << "\n"); | |
| } | |
| void analyzeModule(); | |
| void prepareReverseGraph(); | |
| void computeFixedPoint(); | |
| void optimize(); | |
| template<typename F> void forEachSuccessor(Location loc, F&& f); | |
| #if DAE_STATS | |
| void collectStats(); | |
| #endif // DAE_STATS | |
| void makeUnreferencedFunctionTypes(const std::vector<HeapType>& oldTypes, | |
| const TypeMap& newTypes); | |
| void markParamsUsed(Index funcIndex) { | |
| auto& usages = funcInfos[funcIndex].paramUsages; | |
| std::fill(usages.begin(), usages.end(), used.getTop()); | |
| } | |
| void markParamsUsed(Name func) { markParamsUsed(funcIndices.at(func)); } | |
| void markParamsUsed(HeapType rootType) { | |
| auto& usages = getTypeTreeInfo(rootType).paramUsages; | |
| std::fill(usages.begin(), usages.end(), used.getTop()); | |
| } | |
| void markResultsUsed(Index funcIndex) { | |
| funcInfos[funcIndex].resultUsage = used.getTop(); | |
| } | |
| void markResultsUsed(Name func) { markResultsUsed(funcIndices.at(func)); } | |
| void markResultsUsed(HeapType rootType) { | |
| getTypeTreeInfo(rootType).resultUsage = used.getTop(); | |
| } | |
| Used::Element& elem(Location loc) { | |
| if (auto* l = std::get_if<FuncParamLoc>(&loc)) { | |
| return funcInfos[l->first].paramUsages[l->second]; | |
| } | |
| if (auto* l = std::get_if<FuncResultLoc>(&loc)) { | |
| return funcInfos[*l].resultUsage; | |
| } | |
| if (auto* l = std::get_if<TypeParamLoc>(&loc)) { | |
| assert(l->first == getRootType(l->first)); | |
| return getTypeTreeInfo(l->first).paramUsages[l->second]; | |
| } | |
| if (auto* l = std::get_if<TypeResultLoc>(&loc)) { | |
| assert(*l == getRootType(*l)); | |
| return getTypeTreeInfo(*l).resultUsage; | |
| } | |
| WASM_UNREACHABLE("unexpected location"); | |
| } | |
| }; | |
| struct GraphBuilder : public WalkerPass<ExpressionStackWalker<GraphBuilder>> { | |
| bool isFunctionParallel() override { return true; } | |
| bool modifiesBinaryenIR() override { return false; } | |
| // Analysis lattice. | |
| const Used& used; | |
| // The function info graph is stored as vectors accessed by function index. | |
| // Map function names to their indices. | |
| const std::unordered_map<Name, Index>& funcIndices; | |
| // Vector of analysis info representing the analysis graph we are building. | |
| // This is populated safely in parallel because the visitor for each function | |
| // only modifies the entry for that function. | |
| std::vector<FunctionInfo>& funcInfos; | |
| // The index of the function we are currently walking. | |
| Index index = -1; | |
| // A use of a parameter local does not necessarily imply the use of the | |
| // parameter value. We use a local graph to check where parameter values may | |
| // be used. | |
| std::optional<LazyLocalGraph> localGraph; | |
| bool optimizeReferencedFuncs; | |
| GraphBuilder(const Used& used, | |
| const std::unordered_map<Name, Index>& funcIndices, | |
| std::vector<FunctionInfo>& funcInfos, | |
| bool optimizeReferencedFuncs) | |
| : used(used), funcIndices(funcIndices), funcInfos(funcInfos), | |
| optimizeReferencedFuncs(optimizeReferencedFuncs) {} | |
| std::unique_ptr<Pass> create() override { | |
| return std::make_unique<GraphBuilder>( | |
| used, funcIndices, funcInfos, optimizeReferencedFuncs); | |
| } | |
| void runOnFunction(Module* wasm, Function* func) override { | |
| assert(index == Index(-1)); | |
| index = funcIndices.at(func->name); | |
| localGraph.emplace(func); | |
| WalkerPass<ExpressionStackWalker<GraphBuilder>>::runOnFunction(wasm, func); | |
| } | |
| void visitRefFunc(RefFunc* curr) { | |
| funcInfos[funcIndices.at(curr->func)].referenced = true; | |
| } | |
| void noteContinuation(Type type) { | |
| if (type.isContinuation()) { | |
| funcInfos[index].contTypes.insert(type.getHeapType()); | |
| } | |
| } | |
| void visitResumeHandlers(const ArenaVector<Name>& labels) { | |
| for (Index i = 0; i < labels.size(); ++i) { | |
| if (labels[i]) { | |
| auto* target = findBreakTarget(labels[i]); | |
| assert(target->type.size() >= 1); | |
| auto newContType = target->type[target->type.size() - 1]; | |
| assert(newContType.isContinuation()); | |
| noteContinuation(newContType); | |
| } | |
| } | |
| } | |
| void visitResume(Resume* curr) { | |
| noteContinuation(curr->cont->type); | |
| visitResumeHandlers(curr->handlerBlocks); | |
| } | |
| void visitResumeThrow(ResumeThrow* curr) { | |
| noteContinuation(curr->cont->type); | |
| visitResumeHandlers(curr->handlerBlocks); | |
| } | |
| void visitStackSwitch(StackSwitch* curr) { | |
| noteContinuation(curr->cont->type); | |
| // Do not optimize the return continuation either because that would | |
| // require us to update the type of the switch expression. | |
| if (curr->cont->type.isContinuation()) { | |
| auto retParams = curr->cont->type.getHeapType() | |
| .getContinuation() | |
| .type.getSignature() | |
| .params; | |
| noteContinuation(retParams[retParams.size() - 1]); | |
| } | |
| } | |
| void visitContBind(ContBind* curr) { | |
| noteContinuation(curr->cont->type); | |
| noteContinuation(curr->type); | |
| } | |
| Index getArgIndex(const ExpressionList& operands, Expression* arg) { | |
| for (Index i = 0; i < operands.size(); ++i) { | |
| if (operands[i] == arg) { | |
| return i; | |
| } | |
| } | |
| WASM_UNREACHABLE("expected arg"); | |
| } | |
| void forwardToDirectParam(Location source, | |
| const ExpressionList& operands, | |
| Expression* arg, | |
| Name target) { | |
| auto argIndex = getArgIndex(operands, arg); | |
| auto& forwarded = funcInfos[index].forwardedToDirectParams[target]; | |
| if (forwarded.empty()) { | |
| forwarded.resize(operands.size()); | |
| } | |
| forwarded[argIndex].push_back(source); | |
| } | |
| void forwardToIndirectParam(Location source, | |
| const ExpressionList& operands, | |
| Expression* arg, | |
| HeapType type) { | |
| auto rootType = getRootType(type); | |
| auto argIndex = getArgIndex(operands, arg); | |
| auto& forwarded = funcInfos[index].forwardedToIndirectParams[rootType]; | |
| if (forwarded.empty()) { | |
| forwarded.resize(operands.size()); | |
| } | |
| forwarded[argIndex].push_back(source); | |
| } | |
| void forwardToResult(Location source) { | |
| funcInfos[index].resultSources.push_back(source); | |
| } | |
| // Record the fact that `curr`'s result comes from `source`. | |
| void getValueFromLocation(Expression* curr, Location source) { | |
| // Look at the transitive users of this value (i.e. its parent and further | |
| // ancestors) to see if it flows (possibly with transformations) into | |
| // another location, `dest`. If it is, we say that `src` is "forwarded" to | |
| // `dest`. We will create an edge in the analysis graph so that if `dest` is | |
| // used, then `src` will be marked as used as well. We must make sure the | |
| // current function doesn't first use `src` in other ways, though, for | |
| // example by teeing it to a local or by performing a branching or trapping | |
| // cast on it. As a conservative approximation, consider the `src` used if | |
| // any of the expressions between `curr` and `dest` have non-removable side | |
| // effects (even if those side effects do not depend on the value flowing | |
| // from `curr`). | |
| if (curr == getFunction()->body) { | |
| // No parents to look at, but the result is forwarded to the function | |
| // result. | |
| forwardToResult(source); | |
| return; | |
| } | |
| for (Index i = expressionStack.size() - 1; i > 0; --i) { | |
| auto* expr = expressionStack[i]; | |
| auto* parent = expressionStack[i - 1]; | |
| // TODO: Experiment with caching the location (or lack of location, or | |
| // use) reached from parent expressions so we can avoid traversing the | |
| // same parents more than once. | |
| if (auto* call = parent->dynCast<Call>()) { | |
| forwardToDirectParam(source, call->operands, expr, call->target); | |
| return; | |
| } | |
| if (auto* call = parent->dynCast<CallIndirect>(); | |
| call && expr != call->target && optimizeReferencedFuncs) { | |
| forwardToIndirectParam(source, call->operands, expr, call->heapType); | |
| return; | |
| } | |
| if (auto* call = parent->dynCast<CallRef>(); | |
| call && expr != call->target && optimizeReferencedFuncs) { | |
| if (!call->target->type.isSignature()) { | |
| // The call will never happen, so we don't need to consider it. | |
| return; | |
| } | |
| auto heapType = call->target->type.getHeapType(); | |
| forwardToIndirectParam(source, call->operands, expr, heapType); | |
| return; | |
| } | |
| if (parent->is<Return>()) { | |
| forwardToResult(source); | |
| return; | |
| } | |
| // TODO: Handle unconditional branches to blocks that fall through to the | |
| // end of the function as well? Handle all unconditional branches to | |
| // blocks in general? | |
| // If the value flows into an If condition, we must consider it used | |
| // because removing it may visibly change which arm of the If gets | |
| // executed. This is not captured by the effects analysis below. | |
| if (auto* iff = parent->dynCast<If>(); iff && expr == iff->condition) { | |
| break; | |
| } | |
| // TODO: Skip the effects analysis below when we are flowing out of a | |
| // block. Side effects earlier in the block don't matter. | |
| // If the current parent expression has unremovable side effects, we | |
| // conservatively treat the value as used. | |
| EffectAnalyzer effects(getPassOptions(), *getModule()); | |
| effects.visit(parent); | |
| if (effects.hasUnremovableSideEffects()) { | |
| // Conservatively assume this expression uses the value in some way that | |
| // prevents us from removing it. | |
| break; | |
| } | |
| if (!parent->type.isConcrete()) { | |
| // The value flows no further, so it is not used in an observable way. | |
| return; | |
| } | |
| // If the value flows out of the function body, it is forwarded to this | |
| // function's results. | |
| if (parent == getFunction()->body) { | |
| forwardToResult(source); | |
| return; | |
| } | |
| } | |
| // The value is used by something we aren't analyzing. | |
| if (auto* l = std::get_if<FuncParamLoc>(&source)) { | |
| // Parameter uses are local to the function and can be updated in | |
| // parallel. | |
| funcInfos[index].paramUsages[l->second] = used.getTop(); | |
| } else { | |
| // Other locations will have to be handled later. | |
| funcInfos[index].usedLocations.push_back(source); | |
| } | |
| } | |
| void visitLocalGet(LocalGet* curr) { | |
| if (!getFunction()->isParam(curr->index)) { | |
| return; | |
| } | |
| // A use of a parameter local does not necessarily imply the use of the | |
| // parameter value. Check where the parameter value may be used. | |
| const auto& sets = localGraph->getSets(curr); | |
| bool usesParam = std::any_of( | |
| sets.begin(), sets.end(), [](LocalSet* set) { return set == nullptr; }); | |
| if (!usesParam) { | |
| // The original parameter value does not reach here. | |
| return; | |
| } | |
| funcInfos[index].paramGets.insert(curr); | |
| getValueFromLocation(curr, FuncParamLoc{index, curr->index}); | |
| } | |
| void visitCall(Call* curr) { | |
| if (Intrinsics(*getModule()).isCallWithoutEffects(curr)) { | |
| auto target = curr->operands.back()->cast<RefFunc>()->func; | |
| funcInfos[funcIndices.at(target)].usedInIntrinsic = true; | |
| } | |
| auto* callee = getModule()->getFunction(curr->target); | |
| if (callee->getResults().isConcrete()) { | |
| Location source = FuncResultLoc{funcIndices.at(curr->target)}; | |
| if (curr->isReturn) { | |
| forwardToResult(source); | |
| funcInfos[index].tailCallees.push_back(funcIndices.at(curr->target)); | |
| } else { | |
| getValueFromLocation(curr, source); | |
| } | |
| } | |
| } | |
| void handleIndirectCall(Expression* curr, HeapType type, bool isReturn) { | |
| auto sig = type.getSignature(); | |
| if (sig.results.isConcrete()) { | |
| HeapType rootType = getRootType(type); | |
| Location source = TypeResultLoc{rootType}; | |
| if (isReturn) { | |
| if (optimizeReferencedFuncs) { | |
| forwardToResult(source); | |
| funcInfos[index].tailCalleeTypes.push_back(rootType); | |
| } else { | |
| funcInfos[index].resultUsage = used.getTop(); | |
| } | |
| } else if (optimizeReferencedFuncs) { | |
| getValueFromLocation(curr, source); | |
| } | |
| } | |
| } | |
| void visitCallIndirect(CallIndirect* curr) { | |
| handleIndirectCall(curr, curr->heapType, curr->isReturn); | |
| } | |
| void visitCallRef(CallRef* curr) { | |
| auto targetType = curr->target->type; | |
| if (targetType.isSignature()) { | |
| handleIndirectCall(curr, targetType.getHeapType(), curr->isReturn); | |
| } | |
| } | |
| }; | |
| void DAE2::analyzeModule() { | |
| // Initialize the function infos. (The type infos are initialized | |
| // on-demand instead.) | |
| funcInfos = std::vector<FunctionInfo>(wasm->functions.size()); | |
| for (Index i = 0; i < funcInfos.size(); ++i) { | |
| auto numParams = wasm->functions[i]->getNumParams(); | |
| funcInfos[i].paramUsages.resize(numParams, used.getBottom()); | |
| funcInfos[i].resultUsage = used.getBottom(); | |
| funcInfos[i].paramSources.resize(numParams); | |
| } | |
| // Analyze functions to find forwarded and used parameters as well as | |
| // function references and other relevant information. | |
| GraphBuilder builder(used, funcIndices, funcInfos, optimizeReferencedFuncs); | |
| builder.run(getPassRunner(), wasm); | |
| // Find additional function references at the module level. | |
| builder.walkModuleCode(wasm); | |
| // Update the locations for which we observed direct usage. | |
| for (Index i = 0; i < wasm->functions.size(); ++i) { | |
| auto& info = funcInfos[i]; | |
| for (auto loc : info.usedLocations) { | |
| if (auto* l = std::get_if<FuncResultLoc>(&loc)) { | |
| markResultsUsed(*l); | |
| } else if (auto* l = std::get_if<TypeResultLoc>(&loc)) { | |
| markResultsUsed(*l); | |
| } else { | |
| // Function parameter uses were already handled in parallel. It is | |
| // impossible to directly use a type parameter since they are used only | |
| // transitively through function parameters. | |
| WASM_UNREACHABLE("unexpected location"); | |
| } | |
| } | |
| } | |
| // Model imported and exported functions as referenced so that marking the | |
| // parameters or results of their types as used will prevent optimizations of | |
| // the functions themselves. | |
| for (Index i = 0; i < wasm->functions.size(); ++i) { | |
| if (wasm->functions[i]->imported()) { | |
| funcInfos[i].referenced = true; | |
| } | |
| } | |
| for (auto& export_ : wasm->exports) { | |
| if (export_->kind == ExternalKind::Function) { | |
| auto i = funcIndices.at(*export_->getInternalName()); | |
| funcInfos[i].referenced = true; | |
| } | |
| } | |
| // Functions called with call.without.effects cannot yet be optimized. Mark | |
| // their parameters and results as used. | |
| for (Index i = 0; i < wasm->functions.size(); ++i) { | |
| if (funcInfos[i].usedInIntrinsic) { | |
| markParamsUsed(i); | |
| markResultsUsed(i); | |
| } | |
| } | |
| // JS-called functions will be called externally, so we cannot optimize out | |
| // their parameters or results. | |
| // TODO: Consider optimizing out a suffix of their parameters. | |
| for (auto name : Intrinsics(*wasm).getJSCalledFunctions()) { | |
| markParamsUsed(name); | |
| markResultsUsed(name); | |
| } | |
| // If we're not optimizing referenced functions, mark all their parameters | |
| // and results as used. | |
| if (!optimizeReferencedFuncs) { | |
| for (Index i = 0; i < wasm->functions.size(); ++i) { | |
| if (funcInfos[i].referenced) { | |
| markParamsUsed(i); | |
| markResultsUsed(i); | |
| } | |
| } | |
| } | |
| // Additionally mark parameters of referenced functions with public types (or | |
| // private subtypes of public types) as used because we cannot rewrite their | |
| // types. Similarly, we do not rewrite tag types or function types used in | |
| // continuations, so any referenced function whose type is in the same tree as | |
| // a tag type or continuation function type will have its parameters marked as | |
| // used. | |
| // | |
| // TODO: Consider analyzing whether we can rewrite the types of such | |
| // referenced functions to new private types first. This would require | |
| // analyzing whether they can escape the module. | |
| // | |
| // TODO: Analyze tags and remove their unused parameters. | |
| std::unordered_set<HeapType> unrewritableRoots; | |
| publicHeapTypes = | |
| ModuleUtils::getPublicHeapTypes(*wasm, getPassOptions().worldMode); | |
| for (auto type : publicHeapTypes) { | |
| if (type.isSignature()) { | |
| unrewritableRoots.insert(getRootType(type)); | |
| } | |
| } | |
| for (auto& tag : wasm->tags) { | |
| unrewritableRoots.insert(getRootType(tag->type)); | |
| } | |
| for (Index i = 0; i < wasm->functions.size(); ++i) { | |
| for (auto type : funcInfos[i].contTypes) { | |
| unrewritableRoots.insert(getRootType(type.getContinuation().type)); | |
| } | |
| } | |
| // The types of the call.without.effects imports are excluded from the set of | |
| // public heap types, but until we can handle analyzing and updating them in | |
| // this pass, we must treat them the same as any other imported function | |
| // types. | |
| for (auto& func : wasm->functions) { | |
| if (Intrinsics(*wasm).isCallWithoutEffects(func.get())) { | |
| unrewritableRoots.insert(getRootType(func->type.getHeapType())); | |
| } | |
| } | |
| for (auto root : unrewritableRoots) { | |
| markParamsUsed(root); | |
| markResultsUsed(root); | |
| } | |
| } | |
| void DAE2::prepareReverseGraph() { | |
| // Compute the reverse graph used by the fixed point analysis from the | |
| // forward graph we have built. | |
| // Collect the referenced functions for each type tree. | |
| for (Index i = 0; i < funcInfos.size(); ++i) { | |
| funcInfos[i].paramSources.resize(funcInfos[i].paramUsages.size()); | |
| if (funcInfos[i].referenced) { | |
| auto root = getRootType(wasm->functions[i]->type.getHeapType()); | |
| getTypeTreeInfo(root).referencedFuncs.push_back(i); | |
| } | |
| } | |
| for (Index callerIndex = 0; callerIndex < funcInfos.size(); ++callerIndex) { | |
| auto& callerInfo = funcInfos[callerIndex]; | |
| // Collect the source locations for direct callees. | |
| for (auto& [callee, forwardedParams] : callerInfo.forwardedToDirectParams) { | |
| auto& calleeInfo = funcInfos[funcIndices.at(callee)]; | |
| for (Index destParam = 0; destParam < forwardedParams.size(); | |
| ++destParam) { | |
| for (auto sourceLoc : forwardedParams[destParam]) { | |
| calleeInfo.paramSources[destParam].push_back(sourceLoc); | |
| } | |
| } | |
| } | |
| // Collect the source locations for indirect callees. | |
| for (auto& [calleeRootType, forwardedParams] : | |
| callerInfo.forwardedToIndirectParams) { | |
| assert(getRootType(calleeRootType) == calleeRootType); | |
| auto& typeTreeInfo = getTypeTreeInfo(calleeRootType); | |
| for (Index destParam = 0; destParam < forwardedParams.size(); | |
| ++destParam) { | |
| for (auto sourceLoc : forwardedParams[destParam]) { | |
| typeTreeInfo.paramSources[destParam].push_back(sourceLoc); | |
| } | |
| } | |
| } | |
| // Collect the tail callers of each callee function and type tree. | |
| Location callerResult = FuncResultLoc{callerIndex}; | |
| for (auto calleeIndex : callerInfo.tailCallees) { | |
| funcInfos[calleeIndex].resultSources.push_back(callerResult); | |
| } | |
| for (auto calleeRootType : callerInfo.tailCalleeTypes) { | |
| getTypeTreeInfo(calleeRootType).resultSources.push_back(callerResult); | |
| } | |
| } | |
| } | |
| template<typename F> void DAE2::forEachSuccessor(Location loc, F&& f) { | |
| if (auto* l = std::get_if<TypeParamLoc>(&loc)) { | |
| auto [rootType, paramIndex] = *l; | |
| auto& typeTreeInfo = getTypeTreeInfo(rootType); | |
| // Propagate usage back to locations forwarded from indirect callers. | |
| for (auto source : typeTreeInfo.paramSources[paramIndex]) { | |
| f(source); | |
| } | |
| // Propagate usage to referenced functions with types in the same type tree | |
| // to ensure their types can all be updated uniformly. | |
| for (auto funcIndex : typeTreeInfo.referencedFuncs) { | |
| f(FuncParamLoc{funcIndex, paramIndex}); | |
| } | |
| } else if (auto* l = std::get_if<TypeResultLoc>(&loc)) { | |
| auto& typeTreeInfo = getTypeTreeInfo(*l); | |
| // Propagate usage to referenced functions with types in the same type tree | |
| // to ensure their types can all be updated uniformly. | |
| for (auto funcIndex : typeTreeInfo.referencedFuncs) { | |
| f(FuncResultLoc{funcIndex}); | |
| } | |
| // Propagate to tail callers. | |
| for (auto source : typeTreeInfo.resultSources) { | |
| f(source); | |
| } | |
| } else if (auto* l = std::get_if<FuncParamLoc>(&loc)) { | |
| auto [calleeIndex, calleeParamIndex] = *l; | |
| auto& calleeInfo = funcInfos[calleeIndex]; | |
| // Propagate usage back to locations forwarded from direct callers. | |
| for (auto source : calleeInfo.paramSources[calleeParamIndex]) { | |
| f(source); | |
| } | |
| if (calleeInfo.referenced) { | |
| // Propagate the use to the function type. It will be propagated from | |
| // there to indirect callers and other functions of this type. | |
| auto calleeType = wasm->functions[calleeIndex]->type.getHeapType(); | |
| f(TypeParamLoc{getRootType(calleeType), calleeParamIndex}); | |
| } | |
| } else if (auto* l = std::get_if<FuncResultLoc>(&loc)) { | |
| auto calleeIndex = *l; | |
| auto& calleeInfo = funcInfos[calleeIndex]; | |
| // Propagate usage back to sources of this result, including tail callers. | |
| for (auto source : calleeInfo.resultSources) { | |
| f(source); | |
| } | |
| if (calleeInfo.referenced) { | |
| // Propagate the use to the function type. It will be propagated from | |
| // there to other functions of this type. | |
| auto calleeType = wasm->functions[calleeIndex]->type.getHeapType(); | |
| f(TypeResultLoc{getRootType(calleeType)}); | |
| } | |
| } else { | |
| WASM_UNREACHABLE("unexpected location"); | |
| } | |
| } | |
| // Performs a smallest fixed-point analysis to propagate parameter usage | |
| // information through the reverse call graph. If a parameter is used in a | |
| // function, then any caller parameters that were forwarded to the parameter are | |
| // also used. Cycles of forwarded arguments will not be marked used unless | |
| // one of the arguments starts out as used or there is some source of usage | |
| // outside the cycle. | |
| void DAE2::computeFixedPoint() { | |
| // List of destination locations (i.e. params and results, either of functions | |
| // or root function types) from which we may need to propagate usage | |
| // information. Initialized with all locations we have observed to be used in | |
| // the IR. | |
| // TODO: Consider propagating by connected components instead. | |
| std::vector<Location> work; | |
| for (Index i = 0; i < funcInfos.size(); ++i) { | |
| for (Index j = 0; j < funcInfos[i].paramUsages.size(); ++j) { | |
| if (funcInfos[i].paramUsages[j]) { | |
| work.push_back(FuncParamLoc{i, j}); | |
| } | |
| } | |
| if (funcInfos[i].resultUsage) { | |
| work.push_back(FuncResultLoc{i}); | |
| } | |
| } | |
| for (auto& [rootType, info] : typeTreeInfos) { | |
| for (Index i = 0; i < info.paramUsages.size(); ++i) { | |
| if (info.paramUsages[i]) { | |
| work.push_back(TypeParamLoc{rootType, i}); | |
| } | |
| } | |
| if (info.resultUsage) { | |
| work.push_back(TypeResultLoc{rootType}); | |
| } | |
| } | |
| while (!work.empty()) { | |
| auto loc = work.back(); | |
| work.pop_back(); | |
| auto& e = elem(loc); | |
| assert(e && "unexpected unused location"); | |
| forEachSuccessor(loc, [&](Location succ) { | |
| if (used.join(elem(succ), e)) { | |
| work.push_back(succ); | |
| } | |
| }); | |
| } | |
| } | |
| // Updates function signatures throughout the module. Ensures that all functions | |
| // within the same subtyping tree have the same parameters removed, maintaining | |
| // the validity of the subtyping hierarchy. | |
| struct DAETypeUpdater : GlobalTypeRewriter { | |
| DAE2& parent; | |
| DAETypeUpdater(DAE2& parent) | |
| : GlobalTypeRewriter(*parent.wasm, parent.getPassOptions().worldMode), | |
| parent(parent) {} | |
| void modifySignature(HeapType oldType, Signature& sig) override { | |
| // All signature types in a type tree will have the same parameters removed | |
| // to keep subtyping valid. Look up which parameters to keep by the root | |
| // type in the tree. | |
| auto& info = parent.getTypeTreeInfo(getRootType(oldType)); | |
| auto& usages = info.paramUsages; | |
| bool hasRemoved = std::any_of( | |
| usages.begin(), usages.end(), [&](auto& use) { return !use; }); | |
| if (hasRemoved) { | |
| std::vector<Type> keptParams; | |
| keptParams.reserve(usages.size()); | |
| for (Index i = 0; i < usages.size(); ++i) { | |
| if (usages[i]) { | |
| keptParams.push_back(sig.params[i]); | |
| } | |
| } | |
| sig.params = getTempTupleType(std::move(keptParams)); | |
| } | |
| if (!info.resultUsage) { | |
| sig.results = Type::none; | |
| } | |
| } | |
| // Return the sorted list of old types (used for deterministic ordering) and | |
| // the unordered map from old to new types. | |
| std::pair<std::vector<HeapType>, TypeMap> rebuildTypes() { | |
| auto types = getSortedTypes(getPrivatePredecessors()); | |
| auto map = GlobalTypeRewriter::rebuildTypes(types); | |
| return {std::move(types), std::move(map)}; | |
| } | |
| }; | |
| // Optimize functions in parallel using the DAE2 analysis results. | |
| struct Optimizer | |
| : public WalkerPass< | |
| ExpressionStackWalker<Optimizer, UnifiedExpressionVisitor<Optimizer>>> { | |
| using Super = WalkerPass< | |
| ExpressionStackWalker<Optimizer, UnifiedExpressionVisitor<Optimizer>>>; | |
| bool isFunctionParallel() override { return true; } | |
| // We handle non-nullable local fixups in the pass itself. If we ran the | |
| // fixups after the pass, they could get confused and produce invalid code | |
| // because this pass updates local indices but does not always update function | |
| // types to match. Function types are updated after this pass runs. | |
| bool requiresNonNullableLocalFixups() override { return false; } | |
| const DAE2& parent; | |
| // The info for the function we are running on. | |
| const FunctionInfo* funcInfo = nullptr; | |
| // Map old local indices to new local indices for the function we are | |
| // currently optimizing. Kept parameters and locals may need to have their | |
| // indices shifted down to account for removed parameters, and removed | |
| // parameters will need to be mapped to their new replacement locals. | |
| std::vector<Index> newIndices; | |
| // It is not enough to simply replace removed parameters with locals. Removed | |
| // non-nullable parameters would become non-nullable locals, and those locals | |
| // might have gets that are dropped or forwarded to other optimized calls | |
| // before any set is executed. We cannot in general synthesize a value to | |
| // initialize such locals (nor would we want to), so instead we must make the | |
| // dropped or forwarded gets disappear, as well as their parents up to the | |
| // drop or forwarding call. These are the expressions we must remove. | |
| std::unordered_set<Expression*> removedExpressions; | |
| // We will need to generate fresh labels for trampoline blocks. | |
| std::optional<LabelUtils::LabelManager> labels; | |
| Optimizer(const DAE2& parent) : parent(parent) {} | |
| std::unique_ptr<Pass> create() override { | |
| return std::make_unique<Optimizer>(parent); | |
| } | |
| // Update the locals and local indices within the function. If the function is | |
| // not referenced, also update its type. (Referenced functions will have their | |
| // types updated later in a global type rewriting operation.) | |
| void runOnFunction(Module* wasm, Function* func) override { | |
| if (func->imported()) { | |
| return; | |
| } | |
| funcInfo = &parent.funcInfos[parent.funcIndices.at(func->name)]; | |
| labels.emplace(func); | |
| auto originalType = func->type; | |
| auto originalParams = func->getParams(); | |
| auto numParams = originalParams.size(); | |
| Index numLocals = func->getNumLocals(); | |
| assert(newIndices.empty()); | |
| newIndices.reserve(numLocals); | |
| auto& paramUsages = funcInfo->paramUsages; | |
| assert(paramUsages.size() == numParams); | |
| bool hasRemovedParam = std::any_of( | |
| paramUsages.begin(), paramUsages.end(), [&](auto& use) { return !use; }); | |
| bool hasRemovedResult = | |
| !funcInfo->resultUsage && func->getResults().isConcrete(); | |
| bool hasNewNonNullableLocal = false; | |
| if (hasRemovedParam || hasRemovedResult) { | |
| // Remove the parameters and replace them with scratch locals. Map the | |
| // indices of removed params to their types and names so we can create | |
| // properly typed and named scratch locals later. | |
| std::vector<std::pair<Type, Name>> removedParams; | |
| std::vector<Type> keptParams; | |
| std::unordered_map<Index, Name> newLocalNames; | |
| std::unordered_map<Name, Index> newLocalIndices; | |
| Index next = 0; | |
| for (Index i = 0; i < numLocals; ++i) { | |
| if (i >= numParams || paramUsages[i]) { | |
| // Used param or local. Keep it. | |
| if (i < numParams) { | |
| keptParams.push_back(originalParams[i]); | |
| } | |
| newIndices.push_back(next); | |
| if (auto name = func->getLocalNameOrDefault(i)) { | |
| newLocalNames[next] = name; | |
| newLocalIndices[name] = next; | |
| } | |
| ++next; | |
| } else { | |
| // Skip this unused param and record it to be added as a new local | |
| // later. | |
| removedParams.emplace_back(originalParams[i], | |
| func->getLocalNameOrDefault(i)); | |
| newIndices.push_back(numLocals - removedParams.size()); | |
| } | |
| } | |
| func->localNames = std::move(newLocalNames); | |
| func->localIndices = std::move(newLocalIndices); | |
| // Replace the function's type with a new type using only the kept | |
| // parameters and result. This ensures that newly added locals get the | |
| // right indices. Preserve sharedness in case this becomes the permanent | |
| // new type (when GC is disabled, see below). | |
| Signature sig(Type(keptParams), | |
| funcInfo->resultUsage ? func->getResults() : Type::none); | |
| TypeBuilder builder(1); | |
| builder[0] = sig; | |
| builder[0].setShared(originalType.getHeapType().getShared()); | |
| auto newType = (*builder.build())[0]; | |
| func->type = Type(newType, NonNullable, Exact); | |
| // Add new vars to replace the removed params. | |
| for (Index i = removedParams.size(); i > 0; --i) { | |
| auto& [type, name] = removedParams[i - 1]; | |
| if (type.isNonNullable()) { | |
| hasNewNonNullableLocal = true; | |
| } | |
| Builder::addVar(func, name, type); | |
| } | |
| } else { | |
| // Not changing any indices. | |
| for (Index i = 0; i < numLocals; ++i) { | |
| newIndices.push_back(i); | |
| } | |
| } | |
| Super::runOnFunction(wasm, func); | |
| assert(!hasRemovedResult || !func->body->type.isConcrete()); | |
| // We may have moved pops around. Fix them. | |
| EHUtils::handleBlockNestedPops(func, *wasm); | |
| // We may have introduced a new non-nullable local whose sets do not | |
| // structurally dominate its gets. Fix it. | |
| if (hasNewNonNullableLocal) { | |
| TypeUpdating::handleNonDefaultableLocals(func, *wasm); | |
| } | |
| // If there is a replacement type, install it now. Otherwise we know the | |
| // global type rewriting will do the right thing with the original type, | |
| // so restore it for now to let that rewriting happen. Only do this if we | |
| // are optimizing indirect calls because otherwise there will not be a | |
| // global type rewriting step. | |
| if (funcInfo->replacementType) { | |
| func->type = Type(*funcInfo->replacementType, NonNullable, Exact); | |
| } else if (parent.optimizeReferencedFuncs) { | |
| func->type = originalType; | |
| } | |
| } | |
| // Mark the parents of the current expression for removal until they either | |
| // stop propagating the unused value or reach a call parameter or function | |
| // result. | |
| void markParentsRemoved() { | |
| for (Index i = expressionStack.size() - 1; i > 0; --i) { | |
| auto* parent = expressionStack[i - 1]; | |
| if (parent->is<Call>() || parent->is<CallIndirect>() || | |
| parent->is<CallRef>() || parent->is<Return>()) { | |
| break; | |
| } | |
| if (!removedExpressions.insert(parent).second) { | |
| // This expression, and therefore its relevant parents, have already | |
| // been marked, so we do not need to continue. | |
| break; | |
| } | |
| if (!parent->type.isConcrete()) { | |
| // The value does not flow further, so stop removing. | |
| break; | |
| } | |
| } | |
| } | |
| void visitLocalSet(LocalSet* curr) { curr->index = newIndices[curr->index]; } | |
| void visitLocalGet(LocalGet* curr) { | |
| // If this is a get of a removed parameter, we need to make it disappear | |
| // along with all of its parents until we reach a call or the value is no | |
| // longer propagated. We know removing these expressions is ok because if | |
| // any of them had non-removable side effects, we would not be optimizing | |
| // out the parameter. | |
| if (curr->index < funcInfo->paramUsages.size() && | |
| !funcInfo->paramUsages[curr->index] && | |
| funcInfo->paramGets.contains(curr)) { | |
| removedExpressions.insert(curr); | |
| markParentsRemoved(); | |
| } | |
| curr->index = newIndices[curr->index]; | |
| } | |
| // Given an expression, return a new expression containing all its non-removed | |
| // parts, if any, or otherwise nullptr. The returned expression will have | |
| // non-concrete type because its value will have been removed. | |
| Expression* getReplacement(Expression* curr); | |
| // Given an expression, return its replacement or a nop if it is marked for | |
| // removal, or otherwise return it dropped. | |
| Expression* getReplacementOrDrop(Expression* curr) { | |
| Builder builder(*getModule()); | |
| if (removedExpressions.contains(curr)) { | |
| if (auto* replacement = getReplacement(curr)) { | |
| return replacement; | |
| } | |
| return builder.makeNop(); | |
| } | |
| if (curr->type.isConcrete()) { | |
| return builder.makeDrop(curr); | |
| } | |
| return curr; | |
| } | |
| // Remove operands from any kind of call (i.e. Call, CallIndirect, or | |
| // CallRef), given the call's list of operands and analysis results for those | |
| // operands. | |
| void removeOperands(Expression* call, | |
| ExpressionList& operands, | |
| const Params& usages) { | |
| if (operands.empty()) { | |
| return; | |
| } | |
| assert(usages.empty() || usages.size() == operands.size()); | |
| bool hasRemoved = usages.empty() || | |
| std::any_of(usages.begin(), usages.end(), [](auto& elem) { | |
| return !elem; | |
| }); | |
| if (!hasRemoved) { | |
| return; | |
| } | |
| // In general we cannot change the order of the operands, including the kept | |
| // parts of removed operands, because they may have side effects. Use | |
| // scratch locals to move the kept operand values past any subsequent kept | |
| // parts of removed operands. (N.B. We could use ChildLocalizer here, but | |
| // that would make it hard to find the expression to replace because it | |
| // might or might not end up in a new local.set.) | |
| Builder builder(*getModule()); | |
| Expression* block = nullptr; | |
| Index next = 0; | |
| bool hasUnreachableRemovedOperand = false; | |
| for (Index i = 0; i < operands.size(); ++i) { | |
| auto type = operands[i]->type; | |
| if (!usages.empty() && usages[i]) { | |
| if (type == Type::unreachable) { | |
| // No scratch local necessary to propagate unreachable. | |
| block = builder.blockify(block, operands[i]); | |
| operands[next++] = builder.makeUnreachable(); | |
| continue; | |
| } | |
| Index scratch = Builder::addVar(getFunction(), type); | |
| block = | |
| builder.blockify(block, builder.makeLocalSet(scratch, operands[i])); | |
| operands[next++] = builder.makeLocalGet(scratch, type); | |
| continue; | |
| } | |
| // This operand is removed, but it might have children we need to keep. | |
| if (type == Type::unreachable) { | |
| hasUnreachableRemovedOperand = true; | |
| } | |
| if (auto* replacement = getReplacement(operands[i])) { | |
| block = builder.blockify(block, replacement); | |
| } | |
| } | |
| operands.resize(next); | |
| // If we removed an unreachable operand, then the call is definitely | |
| // unreachable but may no longer have an unreachable child. This is not | |
| // valid, so replace it with an unreachable. | |
| if (hasUnreachableRemovedOperand) { | |
| block = builder.blockify(block, builder.makeUnreachable()); | |
| } else { | |
| block = builder.blockify(block, call); | |
| } | |
| replaceCurrent(block); | |
| } | |
| void visitCall(Call* curr) { | |
| auto& calleeInfo = parent.funcInfos[parent.funcIndices.at(curr->target)]; | |
| if (!calleeInfo.resultUsage && curr->type.isConcrete()) { | |
| curr->type = Type::none; | |
| markParentsRemoved(); | |
| } | |
| removeOperands(curr, curr->operands, calleeInfo.paramUsages); | |
| } | |
| void handleIndirectCall(Expression* curr, | |
| ExpressionList& operands, | |
| HeapType type) { | |
| if (!parent.optimizeReferencedFuncs) { | |
| return; | |
| } | |
| auto it = parent.typeTreeInfos.find(getRootType(type)); | |
| if (it == parent.typeTreeInfos.end()) { | |
| // No analysis results for this type, so none of its parameters or results | |
| // are used. | |
| if (curr->type.isConcrete()) { | |
| curr->type = Type::none; | |
| markParentsRemoved(); | |
| } | |
| removeOperands(curr, operands, {}); | |
| } else { | |
| if (!it->second.resultUsage && curr->type.isConcrete()) { | |
| curr->type = Type::none; | |
| markParentsRemoved(); | |
| } | |
| removeOperands(curr, operands, it->second.paramUsages); | |
| } | |
| } | |
| void visitCallRef(CallRef* curr) { | |
| if (!curr->target->type.isSignature()) { | |
| return; | |
| } | |
| handleIndirectCall(curr, curr->operands, curr->target->type.getHeapType()); | |
| } | |
| void visitCallIndirect(CallIndirect* curr) { | |
| handleIndirectCall(curr, curr->operands, curr->heapType); | |
| } | |
| void visitReturn(Return* curr) { | |
| if (curr->value && !funcInfo->resultUsage) { | |
| // We are removing this function's unused result. Remove or drop the | |
| // original value (depending on whether or not the value uses another | |
| // removed location) and then return. | |
| Builder builder(*getModule()); | |
| replaceCurrent( | |
| builder.makeSequence(getReplacementOrDrop(curr->value), curr)); | |
| curr->value = nullptr; | |
| } | |
| } | |
| void visitExpression(Expression* curr) { | |
| if (curr->type.isConcrete()) { | |
| // If this expression should be removed, that will be handled by the call | |
| // or non-concrete expression it ultimately flows into. | |
| return; | |
| } | |
| if (!removedExpressions.contains(curr)) { | |
| // We're keeping this one. | |
| return; | |
| } | |
| // This is a top-level removed expression. Replace it with its kept | |
| // children, if any, and make sure unreachable expressions remain | |
| // unreachable. | |
| Builder builder(*getModule()); | |
| if (auto* replacement = getReplacement(curr)) { | |
| if (curr->type == Type::unreachable && | |
| replacement->type != Type::unreachable) { | |
| replacement = builder.blockify(replacement, builder.makeUnreachable()); | |
| } | |
| replaceCurrent(replacement); | |
| return; | |
| } | |
| // There are no kept children. | |
| builder.replaceWithIdenticalType(curr); | |
| } | |
| void visitFunction(Function* func) { | |
| // Remove the function result if necessary. | |
| if (func->body->type.isConcrete() && !funcInfo->resultUsage) { | |
| func->body = getReplacementOrDrop(func->body); | |
| } | |
| } | |
| }; | |
| Expression* Optimizer::getReplacement(Expression* curr) { | |
| Builder builder(*getModule()); | |
| if (!removedExpressions.contains(curr)) { | |
| // This expression is not removed, so none of its children are either. (If | |
| // they were, they would already have been removed.) | |
| if (!curr->type.isConcrete()) { | |
| return curr; | |
| } | |
| return builder.makeDrop(curr); | |
| } | |
| // Traverse an expression that is being removed and collect any of its | |
| // sub-expressions that are not being removed into a new expression. | |
| struct Collector | |
| : ExpressionStackWalker<Collector, UnifiedExpressionVisitor<Collector>> { | |
| using Super = | |
| ExpressionStackWalker<Collector, UnifiedExpressionVisitor<Collector>>; | |
| std::unordered_set<Expression*>& removedExpressions; | |
| LabelUtils::LabelManager& labels; | |
| Builder& builder; | |
| // A stack of lists of expressions we are building, one list for each level | |
| // of control flow structures we are inserting them into. | |
| std::vector<std::vector<Expression*>> collectedStack = {{}}; | |
| // Indices indicating which part of a Try we are currently constructing, | |
| // for every Try in the current expression stack. Index 0 is the body and | |
| // index N > 0 is catch body N - 1. | |
| std::vector<Index> tryIndexStack; | |
| Collector(std::unordered_set<Expression*>& removedExpressions, | |
| LabelUtils::LabelManager& labels, | |
| Builder& builder) | |
| : removedExpressions(removedExpressions), labels(labels), | |
| builder(builder) {} | |
| void appendExpr(Expression* curr) { | |
| if (curr->type.isConcrete()) { | |
| curr = builder.makeDrop(curr); | |
| } | |
| collectedStack.back().push_back(curr); | |
| } | |
| Expression* collectExprsOrNull() { | |
| auto& exprs = collectedStack.back(); | |
| Expression* result; | |
| switch (exprs.size()) { | |
| case 0: | |
| return nullptr; | |
| case 1: | |
| result = exprs[0]; | |
| break; | |
| default: | |
| result = builder.makeBlock(exprs, Type::none); | |
| break; | |
| } | |
| exprs.clear(); | |
| return result; | |
| } | |
| Expression* collectExprsOrNop() { | |
| if (auto* expr = collectExprsOrNull()) { | |
| return expr; | |
| } | |
| return builder.makeNop(); | |
| } | |
| void finishControlFlow(Expression* curr) { | |
| collectedStack.pop_back(); | |
| appendExpr(curr); | |
| // Do not traverse this expression again, since it need not change after | |
| // having been processed once. | |
| removedExpressions.erase(curr); | |
| } | |
| // Ifs, Loops, Trys, and labeled Blocks can all produce values and may | |
| // shallowly have only removable side effects, so it is possible that they | |
| // will need to be removed. However, they may contain expressions that | |
| // have non-removable side effects. The execution of these side effects | |
| // must remain under the control of the original control flow structures. | |
| // This custom scan calls hooks that will incorporate the control flow | |
| // structures into the collected expression. Once a control flow structure | |
| // has been fully processed once, remove it from the set of removed | |
| // expressions because its remaining contents are not removed and would | |
| // not change if processed again. | |
| static void scan(Collector* self, Expression** currp) { | |
| Expression* curr = *currp; | |
| if (!self->removedExpressions.contains(curr)) { | |
| // The expressions we are removing form a sub-tree starting at the | |
| // root expression. There is therefore never a removed expression | |
| // inside a non-removed expression. We can just collect this | |
| // non-removed expression without scanning it. | |
| self->appendExpr(curr); | |
| return; | |
| } | |
| if (auto* iff = curr->dynCast<If>()) { | |
| assert(iff->ifFalse && "unexpected one-arm if"); | |
| self->pushTask(doPostVisit, currp); | |
| self->pushTask(doEndIfFalse, currp); | |
| self->pushTask(scan, &iff->ifFalse); | |
| self->pushTask(doEndIfTrue, currp); | |
| self->pushTask(scan, &iff->ifTrue); | |
| // If conditions are always kept, so we do not need to scan them. | |
| self->pushTask(doPreVisit, currp); | |
| self->collectedStack.emplace_back(); | |
| } else if (auto* loop = curr->dynCast<Loop>()) { | |
| self->pushTask(doPostVisit, currp); | |
| self->pushTask(doEndLoop, currp); | |
| self->pushTask(scan, &loop->body); | |
| self->pushTask(doPreVisit, currp); | |
| self->collectedStack.emplace_back(); | |
| } else if (auto* try_ = curr->dynCast<Try>()) { | |
| self->pushTask(doPostVisit, currp); | |
| for (Index i = try_->catchBodies.size(); i > 0; --i) { | |
| self->pushTask(doEndTryBody, currp); | |
| self->pushTask(scan, &try_->catchBodies[i - 1]); | |
| } | |
| self->pushTask(doEndTryBody, currp); | |
| self->pushTask(scan, &try_->body); | |
| self->pushTask(doPreVisit, currp); | |
| self->tryIndexStack.push_back(0); | |
| self->collectedStack.emplace_back(); | |
| } else if (auto* block = curr->dynCast<Block>(); block && block->name) { | |
| self->pushTask(doPostVisit, currp); | |
| self->pushTask(doEndLabeledBlock, currp); | |
| for (Index i = block->list.size(); i > 0; --i) { | |
| self->pushTask(scan, &block->list[i - 1]); | |
| } | |
| self->pushTask(doPreVisit, currp); | |
| self->collectedStack.emplace_back(); | |
| } else { | |
| Super::scan(self, currp); | |
| } | |
| // N.B. No need to handle TryTable because it either has no handlers and | |
| // can be removed or has the branchesOut effect and would not be removed | |
| // in the first place. | |
| } | |
| static void doEndIfFalse(Collector* self, Expression** currp) { | |
| If* curr = (*currp)->cast<If>(); | |
| // If this is null, we will just have erased the empty ifFalse arm, | |
| // which is fine. | |
| curr->ifFalse = self->collectExprsOrNull(); | |
| curr->finalize(); | |
| self->finishControlFlow(curr); | |
| } | |
| static void doEndIfTrue(Collector* self, Expression** currp) { | |
| If* curr = (*currp)->cast<If>(); | |
| // There must be an ifFalse arm because the if must have concrete type | |
| // and we only process each removed control flow structure once. | |
| assert(curr->ifFalse); | |
| curr->ifTrue = self->collectExprsOrNop(); | |
| } | |
| static void doEndLoop(Collector* self, Expression** currp) { | |
| Loop* curr = (*currp)->cast<Loop>(); | |
| curr->body = self->collectExprsOrNop(); | |
| curr->finalize(); | |
| self->finishControlFlow(curr); | |
| } | |
| static void doEndTryBody(Collector* self, Expression** currp) { | |
| Try* curr = (*currp)->cast<Try>(); | |
| Index index = self->tryIndexStack.back()++; | |
| auto* collected = self->collectExprsOrNop(); | |
| if (index == 0) { | |
| curr->body = collected; | |
| } else { | |
| curr->catchBodies[index - 1] = collected; | |
| } | |
| if (index == curr->catchBodies.size()) { | |
| self->tryIndexStack.pop_back(); | |
| curr->finalize(); | |
| self->finishControlFlow(curr); | |
| } | |
| } | |
| static void doEndLabeledBlock(Collector* self, Expression** currp) { | |
| Block* curr = (*currp)->cast<Block>(); | |
| assert(curr->name); | |
| assert(curr->type.isConcrete()); | |
| if (self->collectedStack.back().empty()) { | |
| // This is the happy case where there cannot possibly be branches to | |
| // the block because we have not collected any expressions at all. | |
| // Since an empty block isn't useful, we don't need to collect | |
| // anything new. | |
| self->collectedStack.pop_back(); | |
| return; | |
| } | |
| // The kept expressions might have arbitrary value-carrying branches to | |
| // this block, but we are trying to remove the block's value. In general | |
| // we cannot remove the values from the branches, so we must instead add | |
| // a trampoline that will let us drop the branch values: | |
| // | |
| // (block $fresh ;; A new outer block that returns nothing. | |
| // (drop ;; Drop the branch values. | |
| // (block $l (result t) ;; An inner block for the branches to target. | |
| // ... ;; The kept expressions, if any. | |
| // (br $fresh) ;; If no branches are taken, skip the drop. | |
| // ) | |
| // ) | |
| // ) | |
| Name fresh = self->labels.getUnique("trampoline"); | |
| auto* collected = self->collectExprsOrNull(); | |
| assert(collected); | |
| auto* inner = | |
| self->builder.makeSequence(collected, self->builder.makeBreak(fresh)); | |
| inner->name = curr->name; | |
| inner->type = curr->type; | |
| auto* drop = self->builder.makeDrop(inner); | |
| self->finishControlFlow(self->builder.makeBlock(fresh, drop)); | |
| } | |
| }; | |
| Collector collector(removedExpressions, *labels, builder); | |
| collector.walk(curr); | |
| assert(collector.collectedStack.size() == 1); | |
| return collector.collectExprsOrNull(); | |
| } | |
| void DAE2::optimize() { | |
| Optimizer optimizer(*this); | |
| if (!optimizeReferencedFuncs) { | |
| // Cannot globally rewrite types or optimize referenced functions, so just | |
| // run the optimizer to optimize unreferenced functions. | |
| optimizer.run(getPassRunner(), wasm); | |
| return; | |
| } | |
| TIME(Timer timer); | |
| // Global type rewriting | |
| DAETypeUpdater typeUpdater(*this); | |
| auto [oldTypes, newTypes] = typeUpdater.rebuildTypes(); | |
| TIME(std::cerr << " rebuild types: " << timer.lastElapsed() << "\n"); | |
| makeUnreferencedFunctionTypes(oldTypes, newTypes); | |
| TIME(std::cerr << " make replacements: " << timer.lastElapsed() << "\n"); | |
| optimizer.run(getPassRunner(), wasm); | |
| TIME(std::cerr << " optimize: " << timer.lastElapsed() << "\n"); | |
| // Update the types for referenced functions and all the locations that might | |
| // hold them. The types of non-referenced functions have already been updated | |
| // separately. | |
| typeUpdater.mapTypes(newTypes); | |
| TIME(std::cerr << " update types: " << timer.lastElapsed() << "\n"); | |
| } | |
| // Generate new (possibly optimized) types for each unreferenced function so | |
| // the later global type update will leave them with the desired optimized | |
| // signature. This may involve giving them a different function type that will | |
| // be rewritten to the desired signature, or alternatively we might give the | |
| // function the optimized signature directly and ensure the global type update | |
| // will not change it further. | |
| void DAE2::makeUnreferencedFunctionTypes(const std::vector<HeapType>& oldTypes, | |
| const TypeMap& newTypes) { | |
| auto updateSingleType = [&](Type type) -> Type { | |
| if (!type.isRef()) { | |
| return type; | |
| } | |
| if (auto it = newTypes.find(type.getHeapType()); it != newTypes.end()) { | |
| return type.with(it->second); | |
| } | |
| return type; | |
| }; | |
| auto updateType = [&](Type type) -> Type { | |
| if (type.isTuple()) { | |
| std::vector<Type> elems; | |
| elems.reserve(type.getTuple().size()); | |
| for (auto t : type.getTuple()) { | |
| elems.push_back(updateSingleType(t)); | |
| } | |
| return Type(std::move(elems)); | |
| } | |
| return updateSingleType(type); | |
| }; | |
| auto updateSignature = [&](Signature sig) -> Signature { | |
| sig.params = updateType(sig.params); | |
| sig.results = updateType(sig.results); | |
| return sig; | |
| }; | |
| // Map possibly-optimized signatures with updated types to the pre-rewrite | |
| // heap type that will be rewritten to have those signatures. These are the | |
| // types we will give the unreferenced functions to make sure they end up with | |
| // the correct types after type rewriting. | |
| std::unordered_map<Signature, HeapType> replacementTypes; | |
| // Public types will never be rewritten, so we can use them as replacement | |
| // types when they already have the desired signatures. | |
| for (auto type : publicHeapTypes) { | |
| if (type.isSignature()) { | |
| replacementTypes.insert({type.getSignature(), type}); | |
| } | |
| } | |
| // Other types may be rewritten. We can use them as our replacement types if | |
| // they will be rewritten to have the desired signatures. Do not iterate on | |
| // newTypes directly because it is unordered and we need determinism. | |
| for (auto& oldType : oldTypes) { | |
| if (oldType.isSignature()) { | |
| if (auto it = newTypes.find(oldType); it != newTypes.end()) { | |
| replacementTypes.insert({it->second.getSignature(), oldType}); | |
| } | |
| } | |
| } | |
| // If we can't reuse a type that will be rewritten to the desired signature, | |
| // we need to create a new type that already has the desired signature and | |
| // will not be rewritten. Make sure these new types are distinct from any | |
| // types that will be globally rewritten. | |
| UniqueRecGroups unique(wasm->features); | |
| for (auto& [oldType, newType] : newTypes) { | |
| if (!oldType.isSignature()) { | |
| continue; | |
| } | |
| // New types we generate will always be the first types in their rec groups, | |
| // so we only need to go out of our way to ensure that the new types are | |
| // separate from other function types that are also the first in their rec | |
| // groups. | |
| if (oldType.getRecGroupIndex() != 0) { | |
| continue; | |
| } | |
| if (oldType.getSignature() != newType.getSignature()) { | |
| unique.insertOrGet(oldType.getRecGroup()); | |
| } | |
| } | |
| // Assign replacement types to unreferenced functions that need them. | |
| for (Index i = 0; i < wasm->functions.size(); ++i) { | |
| auto& func = wasm->functions[i]; | |
| if (funcInfos[i].referenced) { | |
| continue; | |
| } | |
| auto params = func->getParams(); | |
| std::vector<Type> keptParams; | |
| keptParams.reserve(params.size()); | |
| for (Index j = 0; j < params.size(); ++j) { | |
| if (funcInfos[i].paramUsages[j]) { | |
| keptParams.push_back(params[j]); | |
| } | |
| } | |
| Type keptResults = func->getResults(); | |
| if (!funcInfos[i].resultUsage) { | |
| keptResults = Type::none; | |
| } | |
| auto newSig = updateSignature({Type(std::move(keptParams)), keptResults}); | |
| // Look up or create a replacement type that will be rewritten to the | |
| // desired signature. | |
| auto [it, inserted] = replacementTypes.insert({newSig, HeapType(0)}); | |
| if (inserted) { | |
| // Create a new type with the desired signature that will not be rewritten | |
| // to some other signature. | |
| // TODO: We need to match sharedness here. | |
| it->second = unique.insert(HeapType(newSig).getRecGroup())[0]; | |
| } | |
| // The global type rewriting will not make the desired update, so we need a | |
| // replacement type. | |
| funcInfos[i].replacementType = it->second; | |
| } | |
| } | |
| #if DAE_STATS | |
| void DAE2::collectStats() { | |
| // Count the number of removed parameters and results of unreferenced | |
| // functions and function types as well as the number of removed parameters | |
| // and results that are part of a forwarding cycle. | |
| Index removedParamUnreferenced = 0, removedParamReferenced = 0, | |
| removedParamType = 0; | |
| Index removedResultUnreferenced = 0, removedResultReferenced = 0, | |
| removedResultType = 0; | |
| Index removedParamCycle = 0, removedTypeParamCycle = 0; | |
| Index removedResultCycle = 0, removedTypeResultCycle = 0; | |
| Index largestCycle = 0, deepestChain = 0; | |
| // Collect a graph of optimized locations and forwarding edges so we can find | |
| // optimized cycles. | |
| using Locations = InsertOrderedSet<Location>; | |
| Locations optimizedLocations; | |
| // Collect unreferenced function parameters/results and total removed | |
| // parameters/results. | |
| for (Index i = 0; i < funcInfos.size(); ++i) { | |
| for (Index j = 0; j < funcInfos[i].paramUsages.size(); ++j) { | |
| if (!funcInfos[i].paramUsages[j]) { | |
| ++(funcInfos[i].referenced ? removedParamReferenced | |
| : removedParamUnreferenced); | |
| optimizedLocations.insert(FuncParamLoc{i, j}); | |
| } | |
| } | |
| if (wasm->functions[i]->getResults() != Type::none && | |
| !funcInfos[i].resultUsage) { | |
| ++(funcInfos[i].referenced ? removedResultReferenced | |
| : removedResultUnreferenced); | |
| optimizedLocations.insert(FuncResultLoc{i}); | |
| } | |
| } | |
| // Collect function type parameters and results. | |
| for (auto& [type, info] : typeTreeInfos) { | |
| assert(getRootType(type) == type); | |
| for (Index i = 0; i < info.paramUsages.size(); ++i) { | |
| if (!info.paramUsages[i]) { | |
| ++removedParamType; | |
| optimizedLocations.insert(TypeParamLoc{type, i}); | |
| } | |
| } | |
| if (type.getSignature().results != Type::none && !info.resultUsage) { | |
| ++removedResultType; | |
| optimizedLocations.insert(TypeResultLoc{type}); | |
| } | |
| } | |
| // Find the strongly-connected components to find cycles. Consider only nodes | |
| // that have been optimized out when computing the graph. | |
| struct LocationSCCs : SCCs<typename Locations::iterator, LocationSCCs> { | |
| DAE2& parent; | |
| Locations& optimizedLocations; | |
| LocationSCCs(DAE2& parent, Locations& optimizedLocations) | |
| : SCCs(optimizedLocations.begin(), optimizedLocations.end()), | |
| parent(parent), optimizedLocations(optimizedLocations) {} | |
| void pushChildren(Location& node) { | |
| parent.forEachSuccessor(node, [&](Location succ) { | |
| if (optimizedLocations.contains(succ)) { | |
| push(succ); | |
| } | |
| }); | |
| } | |
| }; | |
| // Track depths for each location to find the longest chain. | |
| std::unordered_map<Location, Index> depths; | |
| auto getDepth = [&](Location l) -> Index { | |
| if (auto it = depths.find(l); it != depths.end()) { | |
| return it->second; | |
| } | |
| return 0; | |
| }; | |
| for (auto scc : LocationSCCs(*this, optimizedLocations)) { | |
| std::vector<Location> sccNodes(scc.begin(), scc.end()); | |
| // Normally we can just take the SCC size as the cycle size, but SCCs of | |
| // size one are not necessarily cycles at all. Look for self-references to | |
| // determine whether we have a size-1 cycle. | |
| bool isCycle = sccNodes.size() > 1; | |
| // Find the depth of this SCC, which is one more than the max depth of its | |
| // children. | |
| Index maxChildDepth = 0; | |
| for (auto& node : sccNodes) { | |
| forEachSuccessor(node, [&](Location succ) { | |
| maxChildDepth = std::max(maxChildDepth, getDepth(succ)); | |
| if (succ == node) { | |
| isCycle = true; | |
| } | |
| }); | |
| } | |
| Index depth = maxChildDepth + 1; | |
| for (auto& node : sccNodes) { | |
| depths[node] = depth; | |
| } | |
| deepestChain = std::max(deepestChain, depth); | |
| if (isCycle) { | |
| largestCycle = std::max(largestCycle, Index(sccNodes.size())); | |
| for (auto& node : sccNodes) { | |
| if (std::get_if<FuncParamLoc>(&node)) { | |
| ++removedParamCycle; | |
| } else if (std::get_if<FuncResultLoc>(&node)) { | |
| ++removedResultCycle; | |
| } else if (std::get_if<TypeParamLoc>(&node)) { | |
| ++removedTypeParamCycle; | |
| } else if (std::get_if<TypeResultLoc>(&node)) { | |
| ++removedTypeResultCycle; | |
| } else { | |
| WASM_UNREACHABLE("unexpected location"); | |
| } | |
| } | |
| } | |
| } | |
| std::cout << "Total removed parameters: " | |
| << (removedParamUnreferenced + removedParamReferenced) << "\n"; | |
| std::cout << "Parameters removed from unreferenced functions: " | |
| << removedParamUnreferenced << "\n"; | |
| std::cout << "Parameters removed from referenced functions: " | |
| << removedParamReferenced << "\n"; | |
| std::cout << "Parameters removed from inhabited function types: " | |
| << removedParamType << "\n"; | |
| std::cout << "Function parameters removed in forwarding cycles: " | |
| << removedParamCycle << "\n"; | |
| std::cout << "Function type parameters removed in forwarding cycles: " | |
| << removedTypeParamCycle << "\n\n"; | |
| std::cout << "Total removed results: " | |
| << (removedResultUnreferenced + removedResultReferenced) << "\n"; | |
| std::cout << "Results removed from unreferenced functions: " | |
| << removedResultUnreferenced << "\n"; | |
| std::cout << "Results removed from referenced functions: " | |
| << removedResultReferenced << "\n"; | |
| std::cout << "Results removed from inhabited function types: " | |
| << removedResultType << "\n"; | |
| std::cout << "Function results removed in forwarding cycles: " | |
| << removedResultCycle << "\n"; | |
| std::cout << "Function type results removed in forwarding cycles: " | |
| << removedTypeResultCycle << "\n\n"; | |
| std::cout << "Largest forwarding cycle: " << largestCycle << "\n"; | |
| std::cout << "Deepest forwarding chain: " << deepestChain << "\n"; | |
| } | |
| #endif // DAE_STATS | |
| } // anonymous namespace | |
| Pass* createDAE2Pass() { return new DAE2(); } | |
| } // namespace wasm |