Repository navigation
add guaranteed tail call fallback mechanism - #165044
folkertdev wants to merge 7 commits into
Conversation
…stream crates to create a shim from
This way we get it right in most simple generic scenarios, the rest can wait for a follow-up
There was a problem hiding this comment.
Currently the codegen for this function is pretty meh. It doesn't blow the stack, but the indirect calls really obscure the actual logic from the optimizer.
We could use !callees to convey to LLVM the set of functions that the indirect call could correspond to, combined with some way of making the shim address unique (e.g. local_unnamed_addr, or just removing unnamed_addr). Then the attributor pass will remove the indirect calls (but not the definitions because their address is used in a comparison).
In combination with -jump-threading-across-loop-headers this gives good results, e.g. see https://godbolt.org/z/3hsv1TGr9.
Alternatively, we could lower tail_eval as a computed goto using LLVM's indirectbr.
Both of these approaches need the full set of possible callees for a particular indirect call. Accumulating that information is not straightforward, but seems possible.
If we had that information we could also explicitly branch on the address of the next function
pointer and jump to the corresponding block of logic.
In any case, we have options to get at least somewhat decent performance out of this, all with only backend changes.
| // FIXME: evaluate what targets we want to use the fallback by default. | ||
| // | ||
| // // Some architectures just fundamentally cannot guarantee tail calls. | ||
| // match self.arch { | ||
| // Arch::PowerPC | Arch::PowerPC64 | Arch::S390x => return false, | ||
| // | ||
| // // FIXME(explicit_tail_calls) LLVM lies about its support, so we must | ||
| // // validate support across targets manually. | ||
| // _ => {} | ||
| // } | ||
| // | ||
| // // On wasm, guaranteed tail calls require the `tail-call` feature. | ||
| // if self.is_like_wasm { | ||
| // return target_features.contains(&sym::tail_call); | ||
| // } |
There was a problem hiding this comment.
Should we wait with enabling the fallback for these targets, or just go ahead here?
| err.note(format!( | ||
| "the `{}` target does not support guaranteed tail calls, \ | ||
| so `become` is lowered to a trampoline", | ||
| sess.target.llvm_target, | ||
| )); |
There was a problem hiding this comment.
note to self: this is all a bit out of date, and depends on whether we enable the fallback by default on some targets.
|
The job Click to see the possible cause of the failure (guessed by this bot) |
|
cc @scottmcm |
|
☔ The latest upstream changes (presumably #165579) made this pull request unmergeable. Please resolve the merge conflicts by rebasing. |
I've been sitting on this for a while, and really just want to know if this is the right direction. This idea was discussed originally in #t-compiler > `become` failing compilation inside `cfg(feature = "...")`.
In summary, there are targets (s390x, powerpc elfv1, current wasm baseline) that fundamentally cannot implement guaranteed tail calls. There are more backends for which LLVM in practice does not (correctly) implement guaranteed tail calls. The idea is to have a fallback mechanism for those targets that at least makes (mutually) recursive functions not blow the stack.
This is unsatisfying in some important ways: guaranteed tail calls are (beyond functional programming homework exercises) a tool for optimization. This fallback mechanism cannot handle indirect (via a function pointer) calls, while that is the main way that tail calls are used in real-world projects. So, the idea here is that the basic uses of
becomedo just work but practically the value of this mechanism is somewhat debatable. Nevertheless, T-lang expressed that they really want a fallback mechanism.The idea here is that a function that uses
becometo tail call another function is transformed (via our shim mechanism) into a function that returns a continuation: a piece of data describing the call to make next.For example, given these mutually recursive functions:
we derive these shims:
And convert the original body into a call to the
tail_evallang item:Creating the shim for
is_evenrequires that we know what shim is generated foris_odd, hence the limitation that this transformation does not work for indirect calls: we have no way of finding the shim in that case.So, that's the idea. My execution of it probably violates a bunch of MIR assumptions at the moment. This branch needs more testing, etc. But before I spend the time on that, I'd like to validate that this looks roughly right.
Currently the fallback can only be triggered by passing
-Zforce-tail-call-fallback, I didn't yet want to change behavior by default. A lot of the changes are just addingShimKind::TailCallin the right places, and then the logic needed to build the shim (verbose but relatively straightforward).One part I'm less confident in is the query stuff, and whether that is robust across crates. We need to keep the body of a function if a downstream crate may want to generate a shim from it.