@@ -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.
202201struct 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