Skip to content

Commit 9c51f2b

Browse files
authored
metadce fixes (#1329)
* ignore missing imports (the wasm may have already had them optimized out) * handle segments that hold on to globals (root them, for now, as we can't remove segments) * run reorder-functions, as the optimal order may have changed after we dce * fix global, global init, and segment offset reachability * fix import rooting and processing - imports may be imported more than once
1 parent 22f1ce8 commit 9c51f2b

File tree

10 files changed

+193
-65
lines changed

10 files changed

+193
-65
lines changed

src/tools/wasm-metadce.cpp

Lines changed: 114 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -51,16 +51,31 @@ struct MetaDCEGraph {
5151
std::unordered_map<Name, DCENode> nodes;
5252
std::unordered_set<Name> roots;
5353

54-
std::unordered_map<Name, Name> importToDCENode; // import internal name => DCE name
5554
std::unordered_map<Name, Name> exportToDCENode; // export exported name => DCE name
5655
std::unordered_map<Name, Name> functionToDCENode; // function name => DCE name
5756
std::unordered_map<Name, Name> globalToDCENode; // global name => DCE name
5857

59-
std::unordered_map<Name, Name> DCENodeToImport; // reverse maps
60-
std::unordered_map<Name, Name> DCENodeToExport;
58+
std::unordered_map<Name, Name> DCENodeToExport; // reverse maps
6159
std::unordered_map<Name, Name> DCENodeToFunction;
6260
std::unordered_map<Name, Name> DCENodeToGlobal;
6361

62+
// imports are not mapped 1:1 to DCE nodes in the wasm, since env.X might
63+
// be imported twice, for example. So we don't map a DCE node to an Import,
64+
// but rather the module.base pair ("id") for the import.
65+
// TODO: implement this in a safer way, not a string with a magic separator
66+
typedef Name ImportId;
67+
68+
ImportId getImportId(Name module, Name base) {
69+
return std::string(module.str) + " (*) " + std::string(base.str);
70+
}
71+
72+
ImportId getImportId(Name name) {
73+
auto* imp = wasm.getImport(name);
74+
return getImportId(imp->module, imp->base);
75+
}
76+
77+
std::unordered_map<Name, Name> importIdToDCENode; // import module.base => DCE name
78+
6479
Module& wasm;
6580

6681
MetaDCEGraph(Module& wasm) : wasm(wasm) {}
@@ -84,11 +99,13 @@ struct MetaDCEGraph {
8499
nodes[dceName] = DCENode(dceName);
85100
}
86101
for (auto& imp : wasm.imports) {
87-
if (importToDCENode.find(imp->name) == importToDCENode.end()) {
88-
auto dceName = getName("import", imp->name.str);
89-
DCENodeToImport[dceName] = imp->name;
90-
importToDCENode[imp->name] = dceName;
91-
nodes[dceName] = DCENode(dceName);
102+
// only process function and global imports - the table and memory are always there
103+
if (imp->kind == ExternalKind::Function || imp->kind == ExternalKind::Global) {
104+
auto id = getImportId(imp->module, imp->base);
105+
if (importIdToDCENode.find(id) == importIdToDCENode.end()) {
106+
auto dceName = getName("importId", imp->name.str);
107+
importIdToDCENode[id] = dceName;
108+
}
92109
}
93110
}
94111
for (auto& exp : wasm.exports) {
@@ -104,16 +121,72 @@ struct MetaDCEGraph {
104121
if (wasm.getFunctionOrNull(exp->value)) {
105122
node.reaches.push_back(functionToDCENode[exp->value]);
106123
} else {
107-
node.reaches.push_back(importToDCENode[exp->value]);
124+
node.reaches.push_back(importIdToDCENode[getImportId(exp->value)]);
108125
}
109126
} else if (exp->kind == ExternalKind::Global) {
110127
if (wasm.getGlobalOrNull(exp->value)) {
111128
node.reaches.push_back(globalToDCENode[exp->value]);
112129
} else {
113-
node.reaches.push_back(importToDCENode[exp->value]);
130+
node.reaches.push_back(importIdToDCENode[getImportId(exp->value)]);
114131
}
115132
}
116133
}
134+
// Add initializer dependencies
135+
// if we provide a parent DCE name, that is who can reach what we see
136+
// if none is provided, then it is something we must root
137+
struct InitScanner : public PostWalker<InitScanner> {
138+
InitScanner(MetaDCEGraph* parent, Name parentDceName) : parent(parent), parentDceName(parentDceName) {}
139+
140+
void visitGetGlobal(GetGlobal* curr) {
141+
handleGlobal(curr->name);
142+
}
143+
void visitSetGlobal(SetGlobal* curr) {
144+
handleGlobal(curr->name);
145+
}
146+
147+
private:
148+
MetaDCEGraph* parent;
149+
Name parentDceName;
150+
151+
void handleGlobal(Name name) {
152+
Name dceName;
153+
if (getModule()->getGlobalOrNull(name)) {
154+
// its a global
155+
dceName = parent->globalToDCENode[name];
156+
} else {
157+
// it's an import.
158+
dceName = parent->importIdToDCENode[parent->getImportId(name)];
159+
}
160+
if (parentDceName.isNull()) {
161+
parent->roots.insert(parentDceName);
162+
} else {
163+
parent->nodes[parentDceName].reaches.push_back(dceName);
164+
}
165+
}
166+
};
167+
for (auto& global : wasm.globals) {
168+
InitScanner scanner(this, globalToDCENode[global->name]);
169+
scanner.setModule(&wasm);
170+
scanner.walk(global->init);
171+
}
172+
// we can't remove segments, so root what they need
173+
InitScanner rooter(this, Name());
174+
rooter.setModule(&wasm);
175+
for (auto& segment : wasm.table.segments) {
176+
// TODO: currently, all functions in the table are roots, but we
177+
// should add an option to refine that
178+
for (auto& name : segment.data) {
179+
if (wasm.getFunctionOrNull(name)) {
180+
roots.insert(functionToDCENode[name]);
181+
} else {
182+
roots.insert(importIdToDCENode[getImportId(name)]);
183+
}
184+
}
185+
rooter.walk(segment.offset);
186+
}
187+
for (auto& segment : wasm.memory.segments) {
188+
rooter.walk(segment.offset);
189+
}
117190

118191
// A parallel scanner for function bodies
119192
struct Scanner : public WalkerPass<PostWalker<Scanner>> {
@@ -133,7 +206,7 @@ struct MetaDCEGraph {
133206
void visitCallImport(CallImport* curr) {
134207
assert(parent->functionToDCENode.count(getFunction()->name) > 0);
135208
parent->nodes[parent->functionToDCENode[getFunction()->name]].reaches.push_back(
136-
parent->importToDCENode[curr->target]
209+
parent->importIdToDCENode[parent->getImportId(curr->target)]
137210
);
138211
}
139212
void visitGetGlobal(GetGlobal* curr) {
@@ -147,33 +220,23 @@ struct MetaDCEGraph {
147220
MetaDCEGraph* parent;
148221

149222
void handleGlobal(Name name) {
150-
if (getModule()->getGlobalOrNull(name)) return;
151-
// it's an import
152-
parent->nodes[parent->functionToDCENode[getFunction()->name]].reaches.push_back(
153-
parent->importToDCENode[name]
154-
);
223+
if (!getFunction()) return; // non-function stuff (initializers) are handled separately
224+
Name dceName;
225+
if (getModule()->getGlobalOrNull(name)) {
226+
// its a global
227+
dceName = parent->globalToDCENode[name];
228+
} else {
229+
// it's an import.
230+
dceName = parent->importIdToDCENode[parent->getImportId(name)];
231+
}
232+
parent->nodes[parent->functionToDCENode[getFunction()->name]].reaches.push_back(dceName);
155233
}
156234
};
157235

158236
PassRunner runner(&wasm);
159237
runner.setIsNested(true);
160238
runner.add<Scanner>(this);
161239
runner.run();
162-
163-
// also scan segment offsets
164-
Scanner scanner(this);
165-
scanner.setModule(&wasm);
166-
for (auto& segment : wasm.table.segments) {
167-
scanner.walk(segment.offset);
168-
// TODO: currently, all functions in the table are roots, but we
169-
// should add an option to refine that
170-
for (auto& name : segment.data) {
171-
roots.insert(functionToDCENode[name]);
172-
}
173-
}
174-
for (auto& segment : wasm.memory.segments) {
175-
scanner.walk(segment.offset);
176-
}
177240
}
178241

179242
private:
@@ -229,6 +292,7 @@ struct MetaDCEGraph {
229292
// Now they are gone, standard optimization passes can do the rest!
230293
PassRunner passRunner(&wasm);
231294
passRunner.add("remove-unused-module-elements");
295+
passRunner.add("reorder-functions"); // removing functions may alter the optimum order, as # of calls can change
232296
passRunner.run();
233297
}
234298

@@ -253,13 +317,18 @@ struct MetaDCEGraph {
253317
for (auto root : roots) {
254318
std::cout << "root: " << root.str << '\n';
255319
}
320+
std::map<Name, ImportId> importMap;
321+
for (auto& pair : importIdToDCENode) {
322+
auto& id = pair.first;
323+
auto dceName = pair.second;
324+
importMap[dceName] = id;
325+
}
256326
for (auto& pair : nodes) {
257327
auto name = pair.first;
258328
auto& node = pair.second;
259329
std::cout << "node: " << name.str << '\n';
260-
if (DCENodeToImport.find(name) != DCENodeToImport.end()) {
261-
auto* imp = wasm.getImport(DCENodeToImport[name]);
262-
std::cout << " is import " << DCENodeToImport[name] << ", " << imp->module.str << '.' << imp->base.str << '\n';
330+
if (importMap.find(name) != importMap.end()) {
331+
std::cout << " is import " << importMap[name] << '\n';
263332
}
264333
if (DCENodeToExport.find(name) != DCENodeToExport.end()) {
265334
std::cout << " is export " << DCENodeToExport[name].str << ", " << wasm.getExport(DCENodeToExport[name])->value << '\n';
@@ -288,6 +357,7 @@ int main(int argc, const char* argv[]) {
288357
bool emitBinary = true;
289358
bool debugInfo = false;
290359
std::string graphFile;
360+
bool dump = false;
291361

292362
Options options("wasm-metadce", "This tool performs dead code elimination (DCE) on a larger space "
293363
"that the wasm module is just a part of. For example, if you have "
@@ -350,6 +420,9 @@ int main(int argc, const char* argv[]) {
350420
[&](Options* o, const std::string& argument) {
351421
graphFile = argument;
352422
})
423+
.add("--dump", "-d", "Dump the combined graph file (useful for debugging)",
424+
Options::Arguments::Zero,
425+
[&](Options *o, const std::string &arguments) { dump = true; })
353426
.add_positional("INFILE", Options::Arguments::One,
354427
[](Options* o, const std::string& argument) {
355428
o->extra["infile"] = argument;
@@ -439,9 +512,8 @@ int main(int argc, const char* argv[]) {
439512
if (!imp->isArray() || imp->size() != 2 || !imp[0]->isString() || !imp[1]->isString()) {
440513
Fatal() << "node.import, if it exists, must be an array of two strings. see --help for the form";
441514
}
442-
auto importName = ImportUtils::getImport(wasm, imp[0]->getIString(), imp[1]->getIString())->name;
443-
graph.importToDCENode[importName] = node.name;
444-
graph.DCENodeToImport[node.name] = importName;
515+
auto id = graph.getImportId(imp[0]->getIString(), imp[1]->getIString());
516+
graph.importIdToDCENode[id] = node.name;
445517
}
446518
// TODO: optimize this copy with a clever move
447519
graph.nodes[node.name] = node;
@@ -450,6 +522,11 @@ int main(int argc, const char* argv[]) {
450522
// The external graph is now populated. Scan the module
451523
graph.scanWebAssembly();
452524

525+
// Debug dump the graph, if requested
526+
if (dump) {
527+
graph.dump();
528+
}
529+
453530
// Perform the DCE
454531
graph.deadCodeElimination();
455532

test/metadce/corners.wast

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
(module
2+
(import "env" "STACKTOP" (global $STACKTOP$asm2wasm$import i32))
3+
(global $STACKTOP (mut i32) (get_global $STACKTOP$asm2wasm$import))
4+
5+
(import "env" "UNUSEDTOP" (global $UNUSEDTOP$asm2wasm$import i32))
6+
(global $UNUSEDTOP (mut i32) (get_global $UNUSEDTOP$asm2wasm$import))
7+
8+
(import "env" "imported_twice" (func $imported_twice_a)) ;; and used just once,
9+
(import "env" "imported_twice" (func $imported_twice_b)) ;; but the other should not kill the import for both!
10+
11+
(import "env" "an-imported-table-func" (func $imported_table_func))
12+
13+
(import "env" "table" (table 10 10 anyfunc))
14+
(elem (i32.const 0) $imported_table_func)
15+
16+
(export "stackAlloc" (func $stackAlloc))
17+
18+
(func $stackAlloc
19+
(drop (get_global $STACKTOP))
20+
(call $imported_twice_a)
21+
)
22+
)
23+

test/metadce/corners.wast.dced

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
(module
2+
(type $FUNCSIG$v (func))
3+
(import "env" "STACKTOP" (global $STACKTOP$asm2wasm$import i32))
4+
(import "env" "imported_twice" (func $imported_twice_a))
5+
(import "env" "an-imported-table-func" (func $imported_table_func))
6+
(import "env" "table" (table 10 10 anyfunc))
7+
(global $STACKTOP (mut i32) (get_global $STACKTOP$asm2wasm$import))
8+
(elem (i32.const 0) $imported_table_func)
9+
(memory $0 0)
10+
(export "stackAlloc" (func $stackAlloc))
11+
(func $stackAlloc (; 2 ;) (type $FUNCSIG$v)
12+
(drop
13+
(get_global $STACKTOP)
14+
)
15+
(call $imported_twice_a)
16+
)
17+
)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
unused: global$UNUSEDTOP$2
2+
unused: ignorable import
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[
2+
{
3+
"name": "ignorable import",
4+
"import": ["non", "existent"]
5+
},
6+
{
7+
"name": "imported_twice",
8+
"import": ["env", "imported_twice"]
9+
},
10+
{
11+
"name": "stackAllocGood",
12+
"export": "stackAlloc",
13+
"root": true
14+
}
15+
]
16+

test/metadce/no-outside.wast.dced.stdout

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,3 @@ unused: export$wasm_func$4
22
unused: export$wasm_func_unused$5
33
unused: func$a_wasm_func$0
44
unused: func$an_unused_wasm_func$1
5-
unused: import$a_js_func$2
6-
unused: import$an_unused_js_func$3

test/metadce/outside.wast.dced

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@
1111
(data (i32.const 1024) "abcd")
1212
(data (get_global $from_segment) "abcd")
1313
(export "wasm_func" (func $a_wasm_func))
14-
(func $a_wasm_func (; 1 ;) (type $FUNCSIG$v)
14+
(func $table_func (; 1 ;) (type $FUNCSIG$v)
15+
(nop)
16+
)
17+
(func $a_wasm_func (; 2 ;) (type $FUNCSIG$v)
1518
(call $a_js_func)
1619
(drop
1720
(get_global $DYNAMICTOP_PTR$asm2wasm$import)
@@ -20,7 +23,4 @@
2023
(get_global $__THREW__)
2124
)
2225
)
23-
(func $table_func (; 2 ;) (type $FUNCSIG$v)
24-
(nop)
25-
)
2626
)
Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
1-
unused: export$wasm_func_unused$14
1+
unused: export$wasm_func_unused$12
22
unused: func$an_unused_wasm_func$1
3-
unused: global$__THREW__$3
43
unused: global$__THREW__unused$4
54
unused: global$from_segment$5
65
unused: global$from_segment_2$6
76
unused: global$from_segment_never_used$7
8-
unused: import$0$12
9-
unused: import$DYNAMICTOP_PTR$asm2wasm$import_unused$11
10-
unused: import$an_unused_js_func$9
11-
unused: import$import$table$0$13

test/metadce/threaded.wast.dced

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,16 @@
88
(export "wasm_func2" (func $wasm_func_2))
99
(export "wasm_func3" (func $wasm_func_3))
1010
(export "wasm_func4" (func $wasm_func_4))
11-
(func $wasm_func_1 (; 3 ;) (type $FUNCSIG$v)
12-
(call $js_func_2)
13-
)
14-
(func $wasm_func_2 (; 4 ;) (type $FUNCSIG$v)
15-
(call $js_func_3)
11+
(func $wasm_func_4 (; 3 ;) (type $FUNCSIG$v)
12+
(nop)
1613
)
17-
(func $wasm_func_3 (; 5 ;) (type $FUNCSIG$v)
14+
(func $wasm_func_3 (; 4 ;) (type $FUNCSIG$v)
1815
(call $js_func_4)
1916
)
20-
(func $wasm_func_4 (; 6 ;) (type $FUNCSIG$v)
21-
(nop)
17+
(func $wasm_func_2 (; 5 ;) (type $FUNCSIG$v)
18+
(call $js_func_3)
19+
)
20+
(func $wasm_func_1 (; 6 ;) (type $FUNCSIG$v)
21+
(call $js_func_2)
2222
)
2323
)

0 commit comments

Comments
 (0)