Skip to content

Commit 98b720d

Browse files
authored
RemoveUnusedModuleElements: Optimize indirect calls when the table is immutable (#8107)
To do this, refactor the table scanning code out of Directize into TableUtils. Then when we see a CallIndirect in RemoveUnusedModuleElements, we no longer automatically assume it could call anything whose reference was taken: if we see the table is not modified, then only the table's known contents might be called. That is, before: CallIndirect implied we could call anything of that type, in that table. But also, we assumed other things might be written into the table at runtime, so anything whose reference was taken was callable. After: We know which tables do not have new entries written into them.
1 parent b7dc66f commit 98b720d

File tree

5 files changed

+488
-112
lines changed

5 files changed

+488
-112
lines changed

src/ir/table-utils.cpp

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,4 +79,82 @@ bool usesExpressions(ElementSegment* curr, Module* module) {
7979
return !allElementsRefFunc || hasSpecializedType;
8080
}
8181

82+
TableInfoMap computeTableInfo(Module& wasm, bool initialContentsImmutable) {
83+
// Set up the initial info.
84+
TableInfoMap tables;
85+
if (wasm.tables.empty()) {
86+
return tables;
87+
}
88+
for (auto& table : wasm.tables) {
89+
tables[table->name].initialContentsImmutable = initialContentsImmutable;
90+
tables[table->name].flatTable =
91+
std::make_unique<TableUtils::FlatTable>(wasm, *table);
92+
}
93+
94+
// Next, look at the imports and exports.
95+
96+
for (auto& table : wasm.tables) {
97+
if (table->imported()) {
98+
tables[table->name].mayBeModified = true;
99+
}
100+
}
101+
102+
for (auto& ex : wasm.exports) {
103+
if (ex->kind == ExternalKind::Table) {
104+
tables[*ex->getInternalName()].mayBeModified = true;
105+
}
106+
}
107+
108+
// Find which tables have sets, by scanning for instructions. Only do so if we
109+
// might learn anything new.
110+
auto hasUnmodifiableTable = false;
111+
for (auto& [_, info] : tables) {
112+
if (!info.mayBeModified) {
113+
hasUnmodifiableTable = true;
114+
break;
115+
}
116+
}
117+
if (!hasUnmodifiableTable) {
118+
return tables;
119+
}
120+
121+
using TablesWithSet = std::unordered_set<Name>;
122+
123+
ModuleUtils::ParallelFunctionAnalysis<TablesWithSet> analysis(
124+
wasm, [&](Function* func, TablesWithSet& tablesWithSet) {
125+
if (func->imported()) {
126+
return;
127+
}
128+
129+
struct Finder : public PostWalker<Finder> {
130+
TablesWithSet& tablesWithSet;
131+
132+
Finder(TablesWithSet& tablesWithSet) : tablesWithSet(tablesWithSet) {}
133+
134+
void visitTableSet(TableSet* curr) {
135+
tablesWithSet.insert(curr->table);
136+
}
137+
void visitTableFill(TableFill* curr) {
138+
tablesWithSet.insert(curr->table);
139+
}
140+
void visitTableCopy(TableCopy* curr) {
141+
tablesWithSet.insert(curr->destTable);
142+
}
143+
void visitTableInit(TableInit* curr) {
144+
tablesWithSet.insert(curr->table);
145+
}
146+
};
147+
148+
Finder(tablesWithSet).walkFunction(func);
149+
});
150+
151+
for (auto& [_, names] : analysis.map) {
152+
for (auto name : names) {
153+
tables[name].mayBeModified = true;
154+
}
155+
}
156+
157+
return tables;
158+
}
159+
82160
} // namespace wasm::TableUtils

src/ir/table-utils.h

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,53 @@ std::set<Name> getFunctionsNeedingElemDeclare(Module& wasm);
120120
// do so, and some do not, depending on their type and use.)
121121
bool usesExpressions(ElementSegment* curr, Module* module);
122122

123+
// Information about a table's optimizability.
124+
struct TableInfo {
125+
// Whether the table may be modifed at runtime, either because it is imported
126+
// or exported, or table.set operations exist for it in the code.
127+
bool mayBeModified = false;
128+
129+
// Whether we can assume that the initial contents are immutable. That is, if
130+
// a table looks like [a, b, c] in the wasm, and we see a call to index 1, we
131+
// will assume it must call b. It is possible that the table is appended to,
132+
// but in this mode we assume the initial contents are not overwritten. This
133+
// is the case for output from LLVM, for example.
134+
//
135+
// This is a weaker property than mayBeModified (if the table cannot be
136+
// modified at all, we can definitely assume the initial contents we see are
137+
// not mutated), but is useful in the case that things are appended to the
138+
// table (as e.g. dynamic linking does in Emscripten, which passes in a flag
139+
// to set this mode; in general, this is an invariant about the program that
140+
// we must be informed about, not one that we can infer - there can be
141+
// table.sets, for example, and this property implies that those sets never
142+
// overwrite initial data).
143+
bool initialContentsImmutable = false;
144+
145+
std::unique_ptr<TableUtils::FlatTable> flatTable;
146+
147+
// Whether we can optimize using this table's data on the entry level, that
148+
// is, individual entries in the table are known to us, so calls through the
149+
// table with known indexes can be inferred, etc.
150+
bool canOptimizeByEntry() const {
151+
// To infer entries, we require:
152+
// * Either the table can't be modified at all, or it can be modified but
153+
// the initial contents are immutable (so we can optimize those
154+
// contents, even if other things might be appended later, which we
155+
// cannot infer).
156+
// * The table is flat (so we can see what is in it, by index).
157+
return (!mayBeModified || initialContentsImmutable) && flatTable->valid;
158+
}
159+
};
160+
161+
// A map of tables to their info.
162+
using TableInfoMap = std::unordered_map<Name, TableInfo>;
163+
164+
// Compute a map with table optimizability info. We can be told that the initial
165+
// contents of the tables are immutable (that is, existing data is not
166+
// overwritten, but new things may be appended).
167+
TableInfoMap computeTableInfo(Module& wasm,
168+
bool initialContentsImmutable = false);
169+
123170
} // namespace wasm::TableUtils
124171

125172
#endif // wasm_ir_table_h

src/passes/Directize.cpp

Lines changed: 16 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,8 @@
2323
//
2424
// --pass-arg=directize-initial-contents-immutable
2525
//
26-
// then the initial tables' contents are assumed to be immutable. That is, if
27-
// a table looks like [a, b, c] in the wasm, and we see a call to index 1, we
28-
// will assume it must call b. It is possible that the table is appended to, but
29-
// in this mode we assume the initial contents are not overwritten. This is the
30-
// case for output from LLVM, for example.
26+
// then the initial tables' contents are assumed to be immutable (see
27+
// TableUtils::TableInfo).
3128
//
3229

3330
#include <unordered_map>
@@ -46,40 +43,18 @@ namespace wasm {
4643

4744
namespace {
4845

49-
struct TableInfo {
50-
// Whether the table may be modifed at runtime, either because it is imported
51-
// or exported, or table.set operations exist for it in the code.
52-
bool mayBeModified = false;
53-
54-
// Whether we can assume that the initial contents are immutable. See the
55-
// toplevel comment.
56-
bool initialContentsImmutable = false;
57-
58-
std::unique_ptr<TableUtils::FlatTable> flatTable;
59-
60-
bool canOptimize() const {
61-
// We can optimize if:
62-
// * Either the table can't be modified at all, or it can be modified but
63-
// the initial contents are immutable (so we can optimize them).
64-
// * The table is flat.
65-
return (!mayBeModified || initialContentsImmutable) && flatTable->valid;
66-
}
67-
};
68-
69-
using TableInfoMap = std::unordered_map<Name, TableInfo>;
70-
7146
struct FunctionDirectizer : public WalkerPass<PostWalker<FunctionDirectizer>> {
7247
bool isFunctionParallel() override { return true; }
7348

7449
std::unique_ptr<Pass> create() override {
7550
return std::make_unique<FunctionDirectizer>(tables);
7651
}
7752

78-
FunctionDirectizer(const TableInfoMap& tables) : tables(tables) {}
53+
FunctionDirectizer(const TableUtils::TableInfoMap& tables) : tables(tables) {}
7954

8055
void visitCallIndirect(CallIndirect* curr) {
8156
auto& table = tables.at(curr->table);
82-
if (!table.canOptimize()) {
57+
if (!table.canOptimizeByEntry()) {
8358
return;
8459
}
8560
// If the target is constant, we can emit a direct call.
@@ -114,7 +89,7 @@ struct FunctionDirectizer : public WalkerPass<PostWalker<FunctionDirectizer>> {
11489
}
11590

11691
private:
117-
const TableInfoMap& tables;
92+
const TableUtils::TableInfoMap& tables;
11893

11994
bool changedTypes = false;
12095

@@ -123,7 +98,7 @@ struct FunctionDirectizer : public WalkerPass<PostWalker<FunctionDirectizer>> {
12398
// that is, whether we know a direct call target, or we know it will trap, or
12499
// if we know nothing.
125100
CallUtils::IndirectCallInfo getTargetInfo(Expression* target,
126-
const TableInfo& table,
101+
const TableUtils::TableInfo& table,
127102
CallIndirect* original) {
128103
auto* c = target->dynCast<Const>();
129104
if (!c) {
@@ -165,7 +140,7 @@ struct FunctionDirectizer : public WalkerPass<PostWalker<FunctionDirectizer>> {
165140
// with an unreachable.
166141
void makeDirectCall(const std::vector<Expression*>& operands,
167142
Expression* c,
168-
const TableInfo& table,
143+
const TableUtils::TableInfo& table,
169144
CallIndirect* original) {
170145
auto info = getTargetInfo(c, table, original);
171146
if (std::get_if<CallUtils::Unknown>(&info)) {
@@ -211,84 +186,18 @@ struct Directize : public Pass {
211186
auto initialContentsImmutable =
212187
hasArgument("directize-initial-contents-immutable");
213188

214-
// Set up the initial info.
215-
TableInfoMap tables;
216-
for (auto& table : module->tables) {
217-
tables[table->name].initialContentsImmutable = initialContentsImmutable;
218-
tables[table->name].flatTable =
219-
std::make_unique<TableUtils::FlatTable>(*module, *table);
220-
}
221-
222-
// Next, look at the imports and exports.
223-
224-
for (auto& table : module->tables) {
225-
if (table->imported()) {
226-
tables[table->name].mayBeModified = true;
227-
}
228-
}
229-
230-
for (auto& ex : module->exports) {
231-
if (ex->kind == ExternalKind::Table) {
232-
tables[*ex->getInternalName()].mayBeModified = true;
233-
}
234-
}
235-
236-
// This may already be enough information to know that we can't optimize
237-
// anything. If so, skip scanning all the module contents.
238-
auto canOptimize = [&]() {
239-
for (auto& [_, info] : tables) {
240-
if (info.canOptimize()) {
241-
return true;
242-
}
243-
}
244-
return false;
245-
};
246-
247-
if (!canOptimize()) {
248-
return;
249-
}
250-
251-
// Find which tables have sets.
189+
auto tables =
190+
TableUtils::computeTableInfo(*module, initialContentsImmutable);
252191

253-
using TablesWithSet = std::unordered_set<Name>;
254-
255-
ModuleUtils::ParallelFunctionAnalysis<TablesWithSet> analysis(
256-
*module, [&](Function* func, TablesWithSet& tablesWithSet) {
257-
if (func->imported()) {
258-
return;
259-
}
260-
261-
struct Finder : public PostWalker<Finder> {
262-
TablesWithSet& tablesWithSet;
263-
264-
Finder(TablesWithSet& tablesWithSet) : tablesWithSet(tablesWithSet) {}
265-
266-
void visitTableSet(TableSet* curr) {
267-
tablesWithSet.insert(curr->table);
268-
}
269-
void visitTableFill(TableFill* curr) {
270-
tablesWithSet.insert(curr->table);
271-
}
272-
void visitTableCopy(TableCopy* curr) {
273-
tablesWithSet.insert(curr->destTable);
274-
}
275-
void visitTableInit(TableInit* curr) {
276-
tablesWithSet.insert(curr->table);
277-
}
278-
};
279-
280-
Finder(tablesWithSet).walkFunction(func);
281-
});
282-
283-
for (auto& [_, names] : analysis.map) {
284-
for (auto name : names) {
285-
tables[name].mayBeModified = true;
192+
// Stop if we cannot optimize anything.
193+
auto hasOptimizableTable = false;
194+
for (auto& [_, info] : tables) {
195+
if (info.canOptimizeByEntry()) {
196+
hasOptimizableTable = true;
197+
break;
286198
}
287199
}
288-
289-
// Perhaps the new information about tables with sets shows we cannot
290-
// optimize.
291-
if (!canOptimize()) {
200+
if (!hasOptimizableTable) {
292201
return;
293202
}
294203

src/passes/RemoveUnusedModuleElements.cpp

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
#include "ir/module-utils.h"
4646
#include "ir/struct-utils.h"
4747
#include "ir/subtypes.h"
48+
#include "ir/table-utils.h"
4849
#include "ir/utils.h"
4950
#include "pass.h"
5051
#include "support/insert_ordered.h"
@@ -172,11 +173,6 @@ struct Noter : public PostWalker<Noter, UnifiedExpressionVisitor<Noter>> {
172173
// the heap type we call with.
173174
reference({ModuleElementKind::Table, curr->table});
174175
noteIndirectCall(curr->table, curr->heapType);
175-
// Note a possible call of a function reference as well, as something might
176-
// be written into the table during runtime. With precise tracking of what
177-
// is written into the table we could do better here; we could also see
178-
// which tables are immutable. TODO
179-
noteCallRef(curr->heapType);
180176
}
181177

182178
void visitCallRef(CallRef* curr) {
@@ -407,6 +403,8 @@ struct Analyzer {
407403

408404
std::unordered_set<IndirectCall> usedIndirectCalls;
409405

406+
std::optional<TableUtils::TableInfoMap> tableInfoMap;
407+
410408
void useIndirectCall(IndirectCall call) {
411409
auto [_, inserted] = usedIndirectCalls.insert(call);
412410
if (!inserted) {
@@ -422,6 +420,16 @@ struct Analyzer {
422420
for (auto& elem : flatTableInfoMap[table].typeElems[type]) {
423421
reference({ModuleElementKind::ElementSegment, elem});
424422
}
423+
424+
// Note a possible call of a function reference as well, if something else
425+
// might be written into the table during runtime.
426+
// TODO: Add an option for immutable initial content like Directize?
427+
if (!tableInfoMap) {
428+
tableInfoMap = TableUtils::computeTableInfo(*module);
429+
}
430+
if ((*tableInfoMap)[table].mayBeModified) {
431+
useCallRefType(type);
432+
}
425433
}
426434

427435
void useRefFunc(Name func) {

0 commit comments

Comments
 (0)