Use effects for indirect call expressions#8625
Conversation
690ee8d to
75481a7
Compare
189d543 to
2630eff
Compare
75481a7 to
36a4fb5
Compare
|
Seems like JJ + Github don't play well when I'm on a branch based on another branch that had a merge commit. Will fix this after merging the other branch. |
When running in --closed-world, compute effects for indirect calls by unioning the effects of all potential functions of that type. In --closed-world, we assume that all references originate in our module, so the only possible functions that we don't know about are imports. Previously [we gave up on effects analysis](https://github.com/WebAssembly/binaryen/blob/29b2d42e8a748fbe1095696d58a52b7bf83e2253/src/passes/GlobalEffects.cpp#L83-L87) for indirect calls. Yields a very small byte count reduction in calcworker (3799354 - 3799297 = 57 bytes). Also shows no significant difference in Binaryen runtime: (0.1346069 -> 0.13375045 = <1% improvement, probably within noise). We expect more benefits after we're able to share indirect call effects with other passes, since currently they're only seen one layer up for callers of functions that indirectly call functions (see the newly-added tests for examples). Followups: * Share effect information per type with other passes besides just via Function::effects (#8625) * Exclude functions that don't have an address (i.e. functions that aren't the target of ref.func) from effect analysis () * Compute effects more precisely for exact + nullable/non-nullable references Part of #8615.
36a4fb5 to
e65a243
Compare
30a31e1 to
60665f3
Compare
60665f3 to
2156d99
Compare
| // When types are rewritten globally, the target type inherits the effects of | ||
| // source type (see type-updating.cpp). If the type of just one function is | ||
| // rewritten, we don't update this, because such a rewrite is only valid | ||
| // if the function is not the target of an indirect call (otherwise the | ||
| // indirect call would have to be rewritten too). |
There was a problem hiding this comment.
This paragraph can maybe move to type-updating.cpp?
There was a problem hiding this comment.
Alon suggested documenting it here: #8625 (comment)
There is a similar comment in type-updating.cpp:
// Update indirect call effects per type.
// When A is rewritten to B, B inherits the effects of A and A loses its
// effects.The part about individual functions being rewritten isn't relevant there since type-updating.cpp is for global type updates. Let me know if I should add anything else to make it more clear.
There was a problem hiding this comment.
I don't understand the second paragraph here.
It sounds like it might be an optimization (we save an update we don't need) - if so, it doesn't need to be in the header. But it sounds like it also might be an invariant, in which case I am confused?
There was a problem hiding this comment.
It's more a point about correctness than an optimization. If we computed effects for the type A, and then rewrote a function foo's type from A into something else, then it must be true that foo could never have been the target of an indirect call (otherwise this rewrite would be wrong), in which case (after #8644) its effects never would have been included under A anyway. And before #8644, our effects would just be out of date and overly conservative but not wrong.
OTOH if we rewrote a function's type from something else into A, then there's also no need to update anything, because any indirect call targeting A was anyway not targeting that function (even though it looks like it could after the rewrite).
Updated the comment to try to make it more clear.
| @@ -1320,16 +1325,18 @@ class EffectAnalyzer { | |||
| } | |||
| } | |||
There was a problem hiding this comment.
Can't we just blindly merge funcEffects here and deal with exception and tryDepth and isReturn stuff at the end of addCallEffects once and for all? (In this case we may not need addCallEffectsFromGlobalEffects as a separate function after all)
There was a problem hiding this comment.
That's how the code was written before but I found it hard to follow with the two different cases interleaved. I prefer to expand it into the two cases where we have and don't have global effects. If you want to compare:
Lines 718 to 771 in 2f1f55a
7eed45c to
979db05
Compare
|
Will run the fuzzer for a few hours. |
|
Ran 3900 iterations with no issues. |
| const EffectAnalyzer* bodyEffects = nullptr; | ||
| if (auto* target = parent.module.getFunctionOrNull(curr->target); | ||
| target && target->effects) { | ||
| bodyEffects = target->effects.get(); |
There was a problem hiding this comment.
Why is this renamed from target to body? target seems natural given it is computed from curr->target etc.?
There was a problem hiding this comment.
The reason is that bodyEffects represents the effects from running the function body, but not the effects from the call itself (e.g. branchesOut from return_call, trapping from a null pointer or type mismatch in call_indirect, etc.). I wanted to distinguish these two things and I feel that bodyEffects makes this more clear.
| // Populate effects of the function's body that were computed from | ||
| // GlobalEffects. Note that calls may have other effects that aren't | ||
| // captured by the function body of the target (e.g. a call_ref may trap on | ||
| // null refs). |
There was a problem hiding this comment.
Related to the above, these effects may capture more than the function body. E.g. an import from JS may trap due to a type conversion of the parameters or results. (I don't think we compute effects for imports atm, but we could in the future, and probably should.)
There was a problem hiding this comment.
Maybe I can change the comment to say Populate a call's effects using effects that were computed from GlobalEffects. i.e. "using" instead of "effects of the function's body". Does that sound more accurate?
Also, the effects from GlobalEffects really do only cover the function's body at the moment. If there are conversions at the JS boundary then those could be considered part of the body of a generated stub function (which IIUC is conceptually how this works).
Semi-related: are we able to compute effects for imports? I thought we wouldn't be able to determine anything about them unless they happen to be Binaryen intrinsics or string builtins. e.g. do we even know if a given import comes from JS or Wasm?
| } | ||
|
|
||
| if (funcEffects.throws_ && (parent.tryDepth > 0 || curr->isReturn)) { | ||
| auto filteredEffects = funcEffects; |
There was a problem hiding this comment.
| auto filteredEffects = funcEffects; | |
| // We can ignore a throw here, as the parent catches it. | |
| auto filteredEffects = funcEffects; |
Also need to explain the isReturn part. I actually don't know the reason for that off the top of my head?
There was a problem hiding this comment.
Done, restored an earlier comment that explained it:
Lines 747 to 751 in 7b79593
Also added a TODO based on the earlier conversation on this PR: #8625 (comment).
| // When types are rewritten globally, the target type inherits the effects of | ||
| // source type (see type-updating.cpp). If the type of just one function is | ||
| // rewritten, we don't update this, because such a rewrite is only valid | ||
| // if the function is not the target of an indirect call (otherwise the | ||
| // indirect call would have to be rewritten too). |
There was a problem hiding this comment.
I don't understand the second paragraph here.
It sounds like it might be an optimization (we save an update we don't need) - if so, it doesn't need to be in the header. But it sounds like it also might be an invariant, in which case I am confused?
70194ce to
038480b
Compare
|
(Sorry for the force-push, I thought it would be fine to force-push changes to the latest commit that hasn't been looked at yet, but it seems that that still messed with the comment history) |
Part of #8615. After #8609, we compute effects for indirect call expressions, but only reflect this in the call-site via the effects of the
Functionthat contains the indirect call. That let us reason about effects only one layer of indirection away, for example in the following module:If we know that an indirect call to $t can't possibly have any effects (e.g. its only potential target is a nop), we'd be able to optimize away
(call $a)but not the(call_ref)itself, since the effects only got stored in the effects of$a.This PR lets us reason about indirect call effects at the expression level within function bodies by adding a map from HeapType to effects
typeEffectsinwasm::Module. As a result we can completely optimize out thecall_refin the above example.Drive-by fixes:
Correctly setWill follow up in return_call with call.without.effects optimizes incorrectly #8693.branchesOutforreturn_calloncall.without.effects. Previously this would not have abranchesOuteffect which may have allowed incorrect reorderings (we shouldn't move an effectful expression above areturn_callbut we would have allowed this).