Skip to content

Commit bb41564

Browse files
authored
Add ModAsyncify* passes (#2404)
These passes are meant to be run after Asyncify has been run, they modify the output. We can assume that we will always unwind if we reach an import, or that we will never unwind, etc. This is meant to help with lazy code loading, that is, the ability for an initially-downloaded wasm to not contain all the code, and if code not present there is called, we download all the rest and continue with that. That could work something like this: * The wasm is created. It contains calls to a special import for lazy code loading. * Asyncify is run on it. * The initially downloaded wasm is created by running --mod-asyncify-always-and-only-unwind: if the special import for lazy code loading is called, we will definitely unwind, and we won't rewind in this binary. * The lazily downloaded wasm is created by running --mod-asyncify-never-unwind: we will rewind into this binary, but no longer need support for unwinding. (Optionally, there could also be a third wasm, which has not had Asyncify run on it, and which we'd swap to for max speed.) These --mod-asyncify passes allow the optimizer to do a lot of work, especially for the initially downloaded wasm if we have lots of calls to the lazy code loading import. In that case the optimizer will see that those calls unwind, which means the code after them is not reached, potentially making lots of code dead and removable.
1 parent 760904a commit bb41564

11 files changed

+1483
-1
lines changed

src/passes/Asyncify.cpp

Lines changed: 145 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,7 @@
253253
//
254254

255255
#include "ir/effects.h"
256+
#include "ir/find_all.h"
256257
#include "ir/literal-utils.h"
257258
#include "ir/memory-utils.h"
258259
#include "ir/module-utils.h"
@@ -1169,6 +1170,10 @@ struct AsyncifyLocals : public WalkerPass<PostWalker<AsyncifyLocals>> {
11691170

11701171
} // anonymous namespace
11711172

1173+
static std::string getFullImportName(Name module, Name base) {
1174+
return std::string(module.str) + '.' + base.str;
1175+
}
1176+
11721177
struct Asyncify : public Pass {
11731178
void run(PassRunner* runner, Module* module) override {
11741179
bool optimize = runner->options.optimizeLevel > 0;
@@ -1209,7 +1214,7 @@ struct Asyncify : public Pass {
12091214
if (allImportsCanChangeState) {
12101215
return true;
12111216
}
1212-
std::string full = std::string(module.str) + '.' + base.str;
1217+
auto full = getFullImportName(module, base);
12131218
for (auto& listedImport : listedImports) {
12141219
if (String::wildcardMatch(listedImport, full)) {
12151220
return true;
@@ -1341,4 +1346,143 @@ struct Asyncify : public Pass {
13411346

13421347
Pass* createAsyncifyPass() { return new Asyncify(); }
13431348

1349+
// Helper passes that can be run after Asyncify.
1350+
1351+
template<bool neverRewind, bool neverUnwind, bool importsAlwaysUnwind>
1352+
struct ModAsyncify
1353+
: public WalkerPass<LinearExecutionWalker<
1354+
ModAsyncify<neverRewind, neverUnwind, importsAlwaysUnwind>>> {
1355+
bool isFunctionParallel() override { return true; }
1356+
1357+
ModAsyncify* create() override {
1358+
return new ModAsyncify<neverRewind, neverUnwind, importsAlwaysUnwind>();
1359+
}
1360+
1361+
void doWalkFunction(Function* func) {
1362+
// Find the asyncify state name.
1363+
auto* unwind = this->getModule()->getExport(ASYNCIFY_STOP_UNWIND);
1364+
auto* unwindFunc = this->getModule()->getFunction(unwind->value);
1365+
FindAll<GlobalSet> sets(unwindFunc->body);
1366+
assert(sets.list.size() == 1);
1367+
asyncifyStateName = sets.list[0]->name;
1368+
// Walk and optimize.
1369+
this->walk(func->body);
1370+
}
1371+
1372+
// Note that we don't just implement GetGlobal as we may know the value is
1373+
// *not* 0, 1, or 2, but not know the actual value. So what we can say depends
1374+
// on the comparison being done on it, and so we implement Binary and
1375+
// Select.
1376+
1377+
void visitBinary(Binary* curr) {
1378+
// Check if this is a comparison of the asyncify state to a specific
1379+
// constant, which we may know is impossible.
1380+
bool flip = false;
1381+
if (curr->op == NeInt32) {
1382+
flip = true;
1383+
} else if (curr->op != EqInt32) {
1384+
return;
1385+
}
1386+
auto* c = curr->right->dynCast<Const>();
1387+
if (!c) {
1388+
return;
1389+
}
1390+
auto* get = curr->left->dynCast<GlobalGet>();
1391+
if (!get || get->name != asyncifyStateName) {
1392+
return;
1393+
}
1394+
// This is a comparison of the state to a constant, check if we know the
1395+
// value.
1396+
int32_t value;
1397+
auto checkedValue = c->value.geti32();
1398+
if ((checkedValue == int(State::Unwinding) && neverUnwind) ||
1399+
(checkedValue == int(State::Rewinding) && neverRewind)) {
1400+
// We know the state is checked against an impossible value.
1401+
value = 0;
1402+
} else if (checkedValue == int(State::Unwinding) && this->unwinding) {
1403+
// We know we are in fact unwinding right now.
1404+
value = 1;
1405+
unsetUnwinding();
1406+
} else {
1407+
return;
1408+
}
1409+
if (flip) {
1410+
value = 1 - value;
1411+
}
1412+
Builder builder(*this->getModule());
1413+
this->replaceCurrent(builder.makeConst(Literal(int32_t(value))));
1414+
}
1415+
1416+
void visitSelect(Select* curr) {
1417+
auto* get = curr->condition->dynCast<GlobalGet>();
1418+
if (!get || get->name != asyncifyStateName) {
1419+
return;
1420+
}
1421+
// This is a comparison of the state to zero, which means we are checking
1422+
// "if running normally, run this code, but if rewinding, ignore it". If
1423+
// we know we'll never rewind, we can optimize this.
1424+
if (neverRewind) {
1425+
Builder builder(*this->getModule());
1426+
curr->condition = builder.makeConst(Literal(int32_t(0)));
1427+
}
1428+
}
1429+
1430+
void visitCall(Call* curr) {
1431+
unsetUnwinding();
1432+
if (!importsAlwaysUnwind) {
1433+
return;
1434+
}
1435+
auto* target = this->getModule()->getFunction(curr->target);
1436+
if (!target->imported()) {
1437+
return;
1438+
}
1439+
// This is an import that definitely unwinds. Await the next check of
1440+
// the state in this linear execution trace, which we can turn into a
1441+
// constant.
1442+
this->unwinding = true;
1443+
}
1444+
1445+
void visitCallIndirect(CallIndirect* curr) { unsetUnwinding(); }
1446+
1447+
static void doNoteNonLinear(
1448+
ModAsyncify<neverRewind, neverUnwind, importsAlwaysUnwind>* self,
1449+
Expression**) {
1450+
// When control flow branches, stop tracking an unwinding.
1451+
self->unsetUnwinding();
1452+
}
1453+
1454+
void visitGlobalSet(GlobalSet* set) {
1455+
// TODO: this could be more precise
1456+
unsetUnwinding();
1457+
}
1458+
1459+
private:
1460+
Name asyncifyStateName;
1461+
1462+
// Whether we just did a call to an import that indicates we are unwinding.
1463+
bool unwinding = false;
1464+
1465+
void unsetUnwinding() { this->unwinding = false; }
1466+
};
1467+
1468+
//
1469+
// Assume imports that may unwind will always unwind, and that rewinding never
1470+
// happens.
1471+
//
1472+
1473+
Pass* createModAsyncifyAlwaysOnlyUnwindPass() {
1474+
return new ModAsyncify<true, false, true>();
1475+
}
1476+
1477+
//
1478+
// Assume that we never unwind, but may still rewind.
1479+
//
1480+
struct ModAsyncifyNeverUnwind : public Pass {
1481+
void run(PassRunner* runner, Module* module) override {}
1482+
};
1483+
1484+
Pass* createModAsyncifyNeverUnwindPass() {
1485+
return new ModAsyncify<false, true, false>();
1486+
}
1487+
13441488
} // namespace wasm

src/passes/pass.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,13 @@ void PassRegistry::registerPasses() {
180180
"minifies both import and export names, and emits a mapping to "
181181
"the minified ones",
182182
createMinifyImportsAndExportsPass);
183+
registerPass("mod-asyncify-always-and-only-unwind",
184+
"apply the assumption that asyncify imports always unwind, "
185+
"and we never rewind",
186+
createModAsyncifyAlwaysOnlyUnwindPass);
187+
registerPass("mod-asyncify-never-unwind",
188+
"apply the assumption that asyncify never unwinds",
189+
createModAsyncifyNeverUnwindPass);
183190
registerPass("nm", "name list", createNameListPass);
184191
registerPass("no-exit-runtime",
185192
"removes calls to atexit(), which is valid if the C runtime "

src/passes/passes.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ Pass* createOptimizeAddedConstantsPropagatePass();
6969
Pass* createOptimizeInstructionsPass();
7070
Pass* createOptimizeStackIRPass();
7171
Pass* createPickLoadSignsPass();
72+
Pass* createModAsyncifyAlwaysOnlyUnwindPass();
73+
Pass* createModAsyncifyNeverUnwindPass();
7274
Pass* createPostEmscriptenPass();
7375
Pass* createPrecomputePass();
7476
Pass* createPrecomputePropagatePass();

0 commit comments

Comments
 (0)