Skip to content

Commit d792524

Browse files
kripkenradekdoulik
authored andcommitted
OnceReduction: Optimize bodies of trivial "once" functions (WebAssembly#6061)
In particular, if the body just calls another "once" function, then we can skip the early-exit logic.
1 parent a8372db commit d792524

File tree

2 files changed

+568
-60
lines changed

2 files changed

+568
-60
lines changed

src/passes/OnceReduction.cpp

Lines changed: 112 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -197,9 +197,10 @@ struct Scanner : public WalkerPass<PostWalker<Scanner>> {
197197
OptInfo& optInfo;
198198
};
199199

200-
// Information in a basic block. We track relevant expressions, which are calls
201-
// calls to "once" functions, and writes to "once" globals.
200+
// Information in a basic block.
202201
struct BlockInfo {
202+
// We track relevant expressions, which are call to "once" functions, and
203+
// writes to "once" globals.
203204
std::vector<Expression*> exprs;
204205
};
205206

@@ -312,18 +313,16 @@ struct Optimizer
312313
optimizeOnce(set->name);
313314
}
314315
} else if (auto* call = expr->dynCast<Call>()) {
315-
if (optInfo.onceFuncs.at(call->target).is()) {
316+
auto target = call->target;
317+
if (optInfo.onceFuncs.at(target).is()) {
316318
// The global used by the "once" func is written.
317319
assert(call->operands.empty());
318-
optimizeOnce(optInfo.onceFuncs.at(call->target));
320+
optimizeOnce(optInfo.onceFuncs.at(target));
319321
continue;
320322
}
321323

322-
// This is not a call to a "once" func. However, we may have inferred
323-
// that it definitely sets some "once" globals before it returns, and
324-
// we can use that information.
325-
for (auto globalName :
326-
optInfo.onceGlobalsSetInFuncs.at(call->target)) {
324+
// Note as written all globals the called function is known to write.
325+
for (auto globalName : optInfo.onceGlobalsSetInFuncs.at(target)) {
327326
onceGlobalsWritten.insert(globalName);
328327
}
329328
} else {
@@ -439,7 +438,110 @@ struct OnceReduction : public Pass {
439438
lastOnceGlobalsSet = currOnceGlobalsSet;
440439
continue;
441440
}
442-
return;
441+
break;
442+
}
443+
444+
// Finally, apply some optimizations to "once" functions themselves. We do
445+
// this at the end to not modify them as we go, which could confuse the main
446+
// part of this pass right before us.
447+
optimizeOnceBodies(optInfo, module);
448+
}
449+
450+
void optimizeOnceBodies(const OptInfo& optInfo, Module* module) {
451+
// Track which "once" functions we remove the exit logic from, as we cannot
452+
// create loops without exit logic, see below.
453+
std::unordered_set<Name> removedExitLogic;
454+
455+
// Iterate deterministically on functions, as the order matters (since we
456+
// make decisions based on previous actions; see below).
457+
for (auto& func : module->functions) {
458+
if (!optInfo.onceFuncs.at(func->name).is()) {
459+
// This is not a "once" function.
460+
continue;
461+
}
462+
463+
// We optimize the case where the payload is trivial, that is where we
464+
// have this:
465+
//
466+
// function foo() {
467+
// if (!foo$once) return; // two lines of
468+
// foo$once = 1; // early-exit code
469+
// PAYLOAD
470+
// }
471+
//
472+
// And PAYLOAD is simple.
473+
auto* body = func->body;
474+
auto& list = body->cast<Block>()->list;
475+
if (list.size() == 2) {
476+
// No payload at all; we don't need the early-exit code then.
477+
//
478+
// Note that this overlaps with SimplifyGlobals' optimization on
479+
// "read-only-to-write" globals: with no payload, this global is really
480+
// only read in order to write itself, and nothing more, so there is no
481+
// observable behavior we need to preserve, and the global can be
482+
// removed. We might as well handle this case here as well since we've
483+
// done all the work up to here, and it is just one line to implement
484+
// the nopping out. (And doing so here can accelerate the optimization
485+
// pipeline by not needing to wait until the next SimplifyGlobals.)
486+
ExpressionManipulator::nop(body);
487+
continue;
488+
}
489+
if (list.size() != 3) {
490+
// Something non-trivial; too many items for us to consider.
491+
continue;
492+
}
493+
auto* payload = list[2];
494+
if (auto* call = payload->dynCast<Call>()) {
495+
if (optInfo.onceFuncs.at(call->target).is()) {
496+
// All this "once" function does is call another. We do not need the
497+
// early-exit logic in this one, then, because of the following
498+
// reasoning. We are comparing these forms:
499+
//
500+
// // BEFORE
501+
// function foo() {
502+
// if (!foo$once) return; // two lines of
503+
// foo$once = 1; // early-exit code
504+
// bar();
505+
// }
506+
//
507+
// to
508+
//
509+
// // AFTER
510+
// function foo() {
511+
// bar();
512+
// }
513+
//
514+
// The question is whether different behavior can be observed between
515+
// those two. There are two cases, when we enter foo:
516+
//
517+
// 1. foo has been called before. Then we early-exit in BEFORE, and
518+
// in AFTER we call bar which will early-exit (since foo was
519+
// called, which means bar was at least entered, which set its
520+
// global; bar might be on the stack, if it called foo, so it has
521+
// not necessarily fully executed - this is a tricky situation to
522+
// handle in general, like recursive imports of modules in various
523+
// languages - but we do know bar has been *entered*, which means
524+
// the global was set).
525+
// 2. foo has never been called before. In this case in BEFORE we set
526+
// the global and call bar, and in AFTER we also call bar.
527+
//
528+
// Thus, the behavior is the same, and we can remove the early-exit
529+
// lines.
530+
//
531+
// We must be careful of loops, however: If A calls B and B calls A,
532+
// then at least one must keep the early-exit logic, or else they
533+
// would infinitely loop if one is called. To avoid that, we track
534+
// which functions we remove the early-exit logic from, and never
535+
// remove the logic if we are calling such a function. (As a result,
536+
// the order of iteration matters here, and so the outer loop in this
537+
// function must be deterministic.)
538+
if (!removedExitLogic.count(call->target)) {
539+
ExpressionManipulator::nop(list[0]);
540+
ExpressionManipulator::nop(list[1]);
541+
removedExitLogic.insert(func->name);
542+
}
543+
}
544+
}
443545
}
444546
}
445547
};

0 commit comments

Comments
 (0)