| /* | |
| * Copyright 2019 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. | |
| */ | |
| // | |
| // Turn indirect calls into direct calls. This is possible if we know | |
| // the table cannot change, and if we see a constant argument for the | |
| // indirect call's index. | |
| // | |
| // If called with | |
| // | |
| // --pass-arg=directize-initial-contents-immutable | |
| // | |
| // then the initial tables' contents are assumed to be immutable. That is, if | |
| // a table looks like [a, b, c] in the wasm, and we see a call to index 1, we | |
| // will assume it must call b. It is possible that the table is appended to, but | |
| // in this mode we assume the initial contents are not overwritten. This is the | |
| // case for output from LLVM, for example. | |
| // | |
| #include <unordered_map> | |
| #include "call-utils.h" | |
| #include "ir/drop.h" | |
| #include "ir/find_all.h" | |
| #include "ir/table-utils.h" | |
| #include "ir/utils.h" | |
| #include "pass.h" | |
| #include "wasm-builder.h" | |
| #include "wasm-traversal.h" | |
| #include "wasm.h" | |
| namespace wasm { | |
| namespace { | |
| struct TableInfo { | |
| // Whether the table may be modifed at runtime, either because it is imported | |
| // or exported, or table.set operations exist for it in the code. | |
| bool mayBeModified = false; | |
| // Whether we can assume that the initial contents are immutable. See the | |
| // toplevel comment. | |
| bool initialContentsImmutable = false; | |
| std::unique_ptr<TableUtils::FlatTable> flatTable; | |
| bool canOptimize() const { | |
| // We can optimize if: | |
| // * Either the table can't be modified at all, or it can be modified but | |
| // the initial contents are immutable (so we can optimize them). | |
| // * The table is flat. | |
| return (!mayBeModified || initialContentsImmutable) && flatTable->valid; | |
| } | |
| }; | |
| using TableInfoMap = std::unordered_map<Name, TableInfo>; | |
| struct FunctionDirectizer : public WalkerPass<PostWalker<FunctionDirectizer>> { | |
| bool isFunctionParallel() override { return true; } | |
| std::unique_ptr<Pass> create() override { | |
| return std::make_unique<FunctionDirectizer>(tables); | |
| } | |
| FunctionDirectizer(const TableInfoMap& tables) : tables(tables) {} | |
| void visitCallIndirect(CallIndirect* curr) { | |
| auto& table = tables.at(curr->table); | |
| if (!table.canOptimize()) { | |
| return; | |
| } | |
| // If the target is constant, we can emit a direct call. | |
| if (curr->target->is<Const>()) { | |
| std::vector<Expression*> operands(curr->operands.begin(), | |
| curr->operands.end()); | |
| makeDirectCall(operands, curr->target, table, curr); | |
| return; | |
| } | |
| // Emit direct calls for things like a select over constants. | |
| if (auto* calls = CallUtils::convertToDirectCalls( | |
| curr, | |
| [&](Expression* target) { | |
| return getTargetInfo(target, table, curr); | |
| }, | |
| *getFunction(), | |
| *getModule())) { | |
| replaceCurrent(calls); | |
| // Note that types may have changed, as the utility here can add locals | |
| // which require fixups if they are non-nullable, for example. | |
| changedTypes = true; | |
| return; | |
| } | |
| } | |
| void doWalkFunction(Function* func) { | |
| WalkerPass<PostWalker<FunctionDirectizer>>::doWalkFunction(func); | |
| if (changedTypes) { | |
| ReFinalize().walkFunctionInModule(func, getModule()); | |
| } | |
| } | |
| private: | |
| const TableInfoMap& tables; | |
| bool changedTypes = false; | |
| // Given an expression that we will use as the target of an indirect call, | |
| // analyze it and return one of the results of CallUtils::IndirectCallInfo, | |
| // that is, whether we know a direct call target, or we know it will trap, or | |
| // if we know nothing. | |
| CallUtils::IndirectCallInfo getTargetInfo(Expression* target, | |
| const TableInfo& table, | |
| CallIndirect* original) { | |
| auto* c = target->dynCast<Const>(); | |
| if (!c) { | |
| return CallUtils::Unknown{}; | |
| } | |
| Address index = c->value.getUnsigned(); | |
| // Check if index is invalid, or the type is wrong. | |
| auto& flatTable = *table.flatTable; | |
| if (index >= flatTable.names.size()) { | |
| // The index is out of bounds for the initial table's content. This may | |
| // trap, but it may also not trap if the table is modified later (if a | |
| // function is appended to it). | |
| if (!table.mayBeModified) { | |
| return CallUtils::Trap{}; | |
| } else { | |
| // The table may be modified, so it might be appended to. We should only | |
| // get here in the case that the initial contents are immutable, as | |
| // otherwise we have nothing to optimize at all. | |
| assert(table.initialContentsImmutable); | |
| return CallUtils::Unknown{}; | |
| } | |
| } | |
| auto name = flatTable.names[index]; | |
| if (!name.is()) { | |
| return CallUtils::Trap{}; | |
| } | |
| auto* func = getModule()->getFunction(name); | |
| if (original->heapType != func->type) { | |
| return CallUtils::Trap{}; | |
| } | |
| return CallUtils::Known{name}; | |
| } | |
| // Create a direct call for a given list of operands, an expression which is | |
| // known to contain a constant indicating the table offset, and the relevant | |
| // table, if we can. If we can see that the call will trap, instead replace | |
| // with an unreachable. | |
| void makeDirectCall(const std::vector<Expression*>& operands, | |
| Expression* c, | |
| const TableInfo& table, | |
| CallIndirect* original) { | |
| auto info = getTargetInfo(c, table, original); | |
| if (std::get_if<CallUtils::Unknown>(&info)) { | |
| // We don't know anything here. | |
| return; | |
| } | |
| // If the index is invalid, or the type is wrong, we can skip the call and | |
| // emit an unreachable here (with dropped children as needed), since in | |
| // Binaryen it is ok to reorder/replace traps when optimizing (but never to | |
| // remove them, at least not by default). | |
| if (std::get_if<CallUtils::Trap>(&info)) { | |
| replaceCurrent( | |
| getDroppedChildrenAndAppend(original, | |
| *getModule(), | |
| getPassOptions(), | |
| Builder(*getModule()).makeUnreachable(), | |
| DropMode::IgnoreParentEffects)); | |
| changedTypes = true; | |
| return; | |
| } | |
| // Everything looks good! | |
| auto name = std::get<CallUtils::Known>(info).target; | |
| replaceCurrent( | |
| Builder(*getModule()) | |
| .makeCall(name, operands, original->type, original->isReturn)); | |
| } | |
| }; | |
| struct Directize : public Pass { | |
| void run(Module* module) override { | |
| if (module->tables.empty()) { | |
| return; | |
| } | |
| // TODO: consider a per-table option here | |
| auto initialContentsImmutable = | |
| hasArgument("directize-initial-contents-immutable"); | |
| // Set up the initial info. | |
| TableInfoMap tables; | |
| for (auto& table : module->tables) { | |
| tables[table->name].initialContentsImmutable = initialContentsImmutable; | |
| tables[table->name].flatTable = | |
| std::make_unique<TableUtils::FlatTable>(*module, *table); | |
| } | |
| // Next, look at the imports and exports. | |
| for (auto& table : module->tables) { | |
| if (table->imported()) { | |
| tables[table->name].mayBeModified = true; | |
| } | |
| } | |
| for (auto& ex : module->exports) { | |
| if (ex->kind == ExternalKind::Table) { | |
| tables[*ex->getInternalName()].mayBeModified = true; | |
| } | |
| } | |
| // This may already be enough information to know that we can't optimize | |
| // anything. If so, skip scanning all the module contents. | |
| auto canOptimize = [&]() { | |
| for (auto& [_, info] : tables) { | |
| if (info.canOptimize()) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| }; | |
| if (!canOptimize()) { | |
| return; | |
| } | |
| // Find which tables have sets. | |
| using TablesWithSet = std::unordered_set<Name>; | |
| ModuleUtils::ParallelFunctionAnalysis<TablesWithSet> analysis( | |
| *module, [&](Function* func, TablesWithSet& tablesWithSet) { | |
| if (func->imported()) { | |
| return; | |
| } | |
| struct Finder : public PostWalker<Finder> { | |
| TablesWithSet& tablesWithSet; | |
| Finder(TablesWithSet& tablesWithSet) : tablesWithSet(tablesWithSet) {} | |
| void visitTableSet(TableSet* curr) { | |
| tablesWithSet.insert(curr->table); | |
| } | |
| void visitTableFill(TableFill* curr) { | |
| tablesWithSet.insert(curr->table); | |
| } | |
| void visitTableCopy(TableCopy* curr) { | |
| tablesWithSet.insert(curr->destTable); | |
| } | |
| void visitTableInit(TableInit* curr) { | |
| tablesWithSet.insert(curr->table); | |
| } | |
| }; | |
| Finder(tablesWithSet).walkFunction(func); | |
| }); | |
| for (auto& [_, names] : analysis.map) { | |
| for (auto name : names) { | |
| tables[name].mayBeModified = true; | |
| } | |
| } | |
| // Perhaps the new information about tables with sets shows we cannot | |
| // optimize. | |
| if (!canOptimize()) { | |
| return; | |
| } | |
| // We can optimize! | |
| FunctionDirectizer(tables).run(getPassRunner(), module); | |
| } | |
| }; | |
| } // anonymous namespace | |
| Pass* createDirectizePass() { return new Directize(); } | |
| } // namespace wasm |