Add TailCall optimization pass (--tail-call) - #9103
Conversation
Add a top-down walker pass that converts call, call_indirect, and call_ref instructions in tail position into return calls. As it traverses the expression tree, it keeps a set of expressions known to be in tail position because they are children of returns, or the end of blocks or ifs in tail position, etc. When a call is found in tail position, it is turned into a return call. Keep track of the exception handling depth to avoid incorrectly turning calls inside exception handlers into return calls.
|
I've pushed your suggested approach in the last commit (although I would still need to go through and update test comments). It is possible to make the pass simpler, as you say, but IMO it is not a whole lot simpler and it's not worth the loss in optimization power since the original approach was also simple enough. |
|
Thanks for the alternative approach. |
This reverts commit 2338ce6.
|
I've pushed a third approach that uses a proper preorder traversal with custom infrastructure that lets us pass |
| // tail position is propagated down from parents to children. Define our own | ||
| // pre-order traversal task stack, and take the opportunity to pass `isTail` | ||
| // as an extra parameter to each task rather than storing it in a side table. | ||
| template<typename SubType> struct PreWalker { |
There was a problem hiding this comment.
The PreWalker can be extracted from the current pass file and used as a shared helper class.
There was a problem hiding this comment.
This is definitely of general use. Though I'm ok with leaving it here for now until we find another use case.
There was a problem hiding this comment.
I'll leave it here for now. The part where we pass bool isTail as an extra argument would be more complicated to generalize.
kripken
left a comment
There was a problem hiding this comment.
lgtm % comments
I do think an expression stack approach would be simpler, but this looks simple enough, so I don't feel strongly. And looks like this handles all the cases fully.
| } | ||
|
|
||
| Module* module = nullptr; | ||
| Function* func = nullptr; |
There was a problem hiding this comment.
We can use getModule()/getFunction()
| PassRunner runner(module, options); | ||
| runner.setIsNested(true); | ||
| runner.add(create()); | ||
| runner.run(); |
There was a problem hiding this comment.
This duplicated some infrastructure from WalkerPass. I've updated the implementation to use WalkerPass directly instead of duplicating this logic.
| PassRunner runner(module, getPassOptions()); | ||
| runner.setIsNested(true); | ||
| runner.add("dce"); | ||
| runner.runOnFunction(func); |
There was a problem hiding this comment.
Let's remove the manual addition of dce here: In general we don't add such operations in passes, and depend on general opts to handle later.
|
Fuzz bug: (module
(type $0 (func (param externref)))
(rec
(type $1 (func (result i32)))
(type $2 (func (param v128) (result (ref struct))))
)
(type $3 (func))
(import "fuzzing-support" "log-externref" (func $fimport$0 (type $0) (param externref)))
(table $0 47 funcref)
(elem $0 (i32.const 0) $0 $0 $0 $0 $0 $0 $1)
(export "func_99_invoker" (func $2))
(func $0 (type $2) (param $0 v128) (result (ref struct))
(unreachable)
)
(func $1 (type $1) (result i32)
(call $fimport$0
(unreachable)
)
)
(func $2 (type $3)
(drop
(unreachable)
)
(unreachable)
)
) |
|
Looks like that DCE run was more load-bearing than I thought :) |
Add a pass that converts call, call_indirect, and call_ref instructions in tail position into return calls. Whether expressions are in tail position is propagated down from parent expressions to children, so we need to do a pre-order traversal instead of our normal post-order traversal. Add a bespoke PreWalker class that additionally passes
isTailto the various expression visitors. Keep track of the exception handling depth to avoid incorrectly turning calls inside exception handlers into return calls.