diff options
author | Alon Zakai <azakai@google.com> | 2022-09-13 15:10:00 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-09-13 22:10:00 +0000 |
commit | 795a70b02032f57f14d97d7555af97f0e527b4b0 (patch) | |
tree | ec3f0625ddfef2b831b1801990a39eafb912d328 | |
parent | f1a3e682e864fcb827a01d3c5725d847053fe149 (diff) | |
download | binaryen-795a70b02032f57f14d97d7555af97f0e527b4b0.tar.gz binaryen-795a70b02032f57f14d97d7555af97f0e527b4b0.tar.bz2 binaryen-795a70b02032f57f14d97d7555af97f0e527b4b0.zip |
[Exceptions] Optimize in CodePushing even with exceptions thrown (#5028)
We had some concerns about this not working in the past, but thinking about it
now, I believe it is safe to do. Specifically, a throw is either like a break or a return -
either it jumps out to an outer scope (like a break) or it jumps out of the function
(like a return), and both breaks and returns have already been handled here.
This change has some nice effects on J2Wasm output, where there are quite a
lot of throws, which we can now optimize around.
-rw-r--r-- | src/ir/effects.h | 12 | ||||
-rw-r--r-- | src/passes/CodePushing.cpp | 20 | ||||
-rw-r--r-- | test/lit/passes/code-pushing-eh.wast | 71 |
3 files changed, 96 insertions, 7 deletions
diff --git a/src/ir/effects.h b/src/ir/effects.h index d3ae9ba20..ad3ab235d 100644 --- a/src/ir/effects.h +++ b/src/ir/effects.h @@ -881,9 +881,19 @@ public: return effects; } - void ignoreBranches() { + // Ignores all forms of control flow transfers: breaks, returns, and + // exceptions. (Note that traps are not considered relevant here - a trap does + // not just transfer control flow, but can be seen as halting the entire + // program.) + // + // This function matches transfersControlFlow(), that is, after calling this + // method transfersControlFlow() will always return false. + void ignoreControlFlowTransfers() { branchesOut = false; breakTargets.clear(); + throws_ = false; + delegateTargets.clear(); + assert(!transfersControlFlow()); } private: diff --git a/src/passes/CodePushing.cpp b/src/passes/CodePushing.cpp index 682f025c8..5026773d4 100644 --- a/src/passes/CodePushing.cpp +++ b/src/passes/CodePushing.cpp @@ -161,11 +161,21 @@ private: // everything that matters if you want to be pushed past the pushPoint EffectAnalyzer cumulativeEffects(passOptions, module); cumulativeEffects.walk(list[pushPoint]); - // it is ok to ignore the branching here, that is the crucial point of this - // opt - // TODO: it would be ok to ignore thrown exceptions here, if we know they - // could not be caught and must go outside of the function - cumulativeEffects.ignoreBranches(); + // It is ok to ignore branching out of the block here, that is the crucial + // point of this optimization. That is, we are in a situation like this: + // + // { + // x = value; + // if (..) break; + // foo(x); + // } + // + // If the branch is taken, then that's fine, it will jump out of this block + // and reach some outer scope, and in that case we never need x at all + // (since we've proven before that x is not used outside of this block, see + // numGetsSoFar which we use for that). Similarly, control flow could + // transfer away via a return or an exception and that would be ok as well. + cumulativeEffects.ignoreControlFlowTransfers(); std::vector<LocalSet*> toPush; Index i = pushPoint - 1; while (1) { diff --git a/test/lit/passes/code-pushing-eh.wast b/test/lit/passes/code-pushing-eh.wast index c428d757d..294c76898 100644 --- a/test/lit/passes/code-pushing-eh.wast +++ b/test/lit/passes/code-pushing-eh.wast @@ -60,7 +60,8 @@ (func $cannot-push-past-throw (local $x i32) (block $out - ;; This local.set cannot be pushed down, because there is 'throw' below + ;; This local.set cannot be pushed down, because there is 'throw' below. + ;; This pass only pushes past conditional control flow atm. (local.set $x (i32.const 1)) (throw $e (i32.const 0)) (drop (i32.const 1)) @@ -322,4 +323,72 @@ (drop (local.get $x)) ) ) + + ;; CHECK: (func $can-push-past-conditional-throw (param $param i32) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (block $block + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $param) + ;; CHECK-NEXT: (throw $e + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $can-push-past-conditional-throw (param $param i32) + (local $x i32) + (block $block + ;; We can push past an if containing a throw. The if is conditional + ;; control flow, which is what we look for in this optimization, and a + ;; throw is like a break - it will jump out of the current block - so we + ;; can push the set past it, as the set is only needed in this block. + (local.set $x (i32.const 1)) + (if + (local.get $param) + (throw $e (i32.const 0)) + ) + (drop (local.get $x)) + ) + ) + + ;; CHECK: (func $cannot-push-past-conditional-throw-extra-use (param $param i32) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (block $block + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $param) + ;; CHECK-NEXT: (throw $e + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $cannot-push-past-conditional-throw-extra-use (param $param i32) + (local $x i32) + ;; As above, but now there is another local.get outside of the block. That + ;; means the local.set cannot be pushed to a place it might not execute. + (block $block + (local.set $x (i32.const 1)) + (if + (local.get $param) + (throw $e (i32.const 0)) + ) + (drop (local.get $x)) + ) + (drop (local.get $x)) + ) ) |