-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
guaranteed tail call elimination #271
Comments
In the PR several people mentioned that they preferred I'm not suggesting to implement tail call elimination, just reserving the keyword for the future. |
What's the state of this? |
@ticki status is "nobody has submitted a new RFC yet" |
I might make one when I get time, if no-one is working on this. |
In Scala, annotating a method with Would something similar be a good fit for Rust (e.g. tagging functions with |
@hatahet we reserved a 'become' keyword, the prevailing idea seems to be that |
Does that mean that having
otherwise, what if it were mixed as follows:
|
I would imagine so, but that's the work that an RFC proposing the syntax would have to work through :) |
How about we first implement this with a trampoline¹ as a slow cross-platform fallback implementation, and then successively implement faster methods for each architecture/platform? ¹ see here for a small and to-be-generalized example trampoline: https://gist.github.com/ConnyOnny/5917e4e549056f6a3db01a5277480f6b |
I'm new to the Rust community, but would be forever sad if I didn't mention this before we committed to this path-- I am coming from the C++ community, where there is a strong (perhaps too strong) reticence to introduce new keywords. TL;DR - abandon I understand that TCO would not be enabled by default for debug builds, but would for (optimized) release builds. A compiler switch would be provided for explicit control (for example, when a user wanted to selectively disable just TCO, but retain other optimizations). If there is someone willing to guide with a Rust newb, I'm willing to work an RFC for this. |
@BradleyGibson TCO can already happen in some cases (i.e. the compiler might optimise tail calls if it decides so). |
@BradleyGibson Last time I looked at it, the impression I got was that we're going in this direction:
Given that EDIT: ...and @ranma42 beat me to it by less than a second. |
This kinda goes against the explicitness of rust.
Guaranteed tail-calls if anything make stuff slower. The only benefit they have are guaranteed bounded stack usage. And there’s nothing preventing compiler from replacing
There’s a considerable amount of work going on already. Most notably this and the corresponding prototyping effort (turned out to be quite a bag of snakes). |
@ranma42 Ah, that is good for me to know. :) Ok, then, I'm cool with |
As always, it is a matter of degrees. I don't think anyone would propose creating a new keyword requiring developers to opt in for every optimization, so (for me) the real question is why would we require users to opt-in to this particular optimization? I'm not sure we are (based on @ranma42's reply), but if this is what you mean, always-opt-in doesn't scale particularly well. And if not always, when?
In most cases the real benefit is the stack, not speed, I agree. There are exceptions, though, and ideally the optimizer will do the right thing under the right circumstances. In any case, you are correct and I've updated my post to reflect this.
Great, I'll definitely take a look. Thanks! |
@ssokolow that sounds perfect to me. Thanks for the summary. |
Combinator to merge two futures yielding the same types
A #[tailrec] atribute decorating the function doesn't require a reserved word, and is easier to read and declare. It is also declarative, you don't need to modify your executable code to check an optimization. If the compiler can't optimize a #[tailrec] function, it raises an error (and maybe a suggestion). The annotation @tailrec works fine in Scala, I think Rust should follow the same approach. |
The problem with an annotation is that a function may include an optimizable and a non-optimizable return. Consider fn foo(x: i32, accu: i32) -> i32 {
if x < 0 {
// not able to TCO because of the +accu after the call
// but also not required because this happens only once => no stack overflow
return foo(-x,1) + accu;
} else {
// needs TCO because the stack could easily overflow
become foo(x-1, x*accu);
}
} If we'd try to solve this with an annotation and two regular The It is also superior to "just apply TCO wherever possible" because sometimes you know "if this is not tail call optimized, the stack will overflow for sure", so you must get a compile error about it, and not a runtime error! |
They way Scala's @tailrec annotation works, is by raising a compiler error if the function cannot be optimized into a loop. Thus you will never get a non-optimizable function, since it won't compile. Additionally, the compiler would suggest you how to change your code to set your return values in optimizable position. Another quirk with the become keyword, is that it is not symmetric with return keyword omission. It hits against expression/functional code styling, while you are trying to use a very functional construction block as recursion is. fn foo(x: i32, accu: i32) -> i32 {
if x < 0 {
// return omission
foo(-x,1) + accu
} else {
// asimmetry
become foo(x-1, x*accu);
}
} |
@ConnyOnny addition is commutative, so with help from the optimizer, shouldn't |
It's not that the |
What's blocking this RFC? Are there unresolved issues? |
any update on this RFC? please provide some update if possible. @steveklabnik @alexcrichton @aturon |
@alkis @0zero0zero this isn't an RFC, it's an issue. As @glaebhoerl said above, #1888 is the actual RFC. you can see the latest status over there. |
@nagisa, I thought it had been established 40 years ago that TCO doesn't make anything slower. It's actually a technique to make things faster… (AFAIK, nothing can be faster than a JMP!) @U007D, just making TCO a possibility pretty much defeats the purpose. If I write a state machine with TCO in mind, there could be billions of recursive calls. If TCO isn't guaranteed, that program is pretty much guaranteed to stack overflow. |
@U007D If TCO is possible but not guaranteed, there must be an artificial recursion limit when TCO is used. Otherwise you'd have code that works or not works depending on random compiler heuristics. |
@le-jzr There are problems with that:
That said, I'd be all for it if you meant "in debug builds, ensure that the non-mandatory TCO optimization is either disabled or instrumented to continue enforcing the recursion limit". That'd be analogous to the overflow/underflow checks we already have. |
@ssokolow I'm not aware of any other optimization that behaves like this. Can you make an example for your first point? For the second one, counting a number is much less overhead than calling a function without TCO, so the net performance is still a lot better than you have any right to expect from an unreliable optimization. That being said, if the recursion limit is enforced only in debug builds, that's fine by me. |
@le-jzr For example, inlining optimizations can behave like this, depending on register pressure, hot-path threshold analysis or resulting binary size constraints. My thinking is that non-mandatory TCO won't make the situation worse than not having any TCO at all in terms of stack usage, so I'm looking at it exactly as you and @ssokolow are suggesting--an optimization. From the perspective of predictability, I agree there could be an issue with this (like many optimizations) and also support the ability to disable and/or enforce recursion limits in debug. |
"like this" in what sense? If you don't explicitly ask for guaranteed TCO, whether your program exhausts the stack depends on whether the TCO optimizer kicks in. If you don't explicitly warn the compiler about accesses to volatile memory, whether your program works or silently corrupts data or crashes is entirely dependent on the internal implementation details of passes like the loop-invariant code motion and redundant instruction combining optimizations. When it comes to benchmarking, one of the first things you'll be taught to watch out for is the loop-deletion pass removing your entire test because the only used result it produces is the time it took to run. On the embedded side, whether the code fits into the microcontroller can be determined by the vagarities of optimizer internals. |
@ssokolow What I mean is that TCO effectively removes a resource limit. It does not just increase the limit by a finite amount, it eliminates it completely/turns it infinite, and this happens in safe code (in contrast to your examples). In practice, what might happen is that you test and debug your application with arbitrarily large data and everything works flawlessly, and then it doesn't work at all on the user's side, and you are left confused, unable to reproduce the issue. |
I see your point and it definitely sounds like it would be well-suited to being handled the same way overflow checks are. |
@U007D, does inlining changes the correctness of the program? I don't think it does. Whether the code is inlined or not, the program should still pass or fail the same tests. This is not true if you write TCO-dependent code and TCO is or isn't done. |
Exactly that, some algorithms depend on guaranteed TCO. |
@kephas I agree completely. I brought up inlining as a response to the question of what other optimizations "works or not works depending on random compiler heuristics". Of course no good optimization would change the correctness of the program. I am saying that in addition to the needs of TCO-dependent code, non-TCO-dependent code can also benefit--I would like to see that happen too. |
We're not doing postponement issues anymore; just continue discussion over at #1888. |
@nixpulvis It's probably not on the roadmap this year. |
That's fine, I'm interested in Rust for the long haul ;) Been watching these issues for years now. |
is there any planning for proper TCO? |
@shirshak55 look at #1888 |
Tracking issue for postponed PR #81
The text was updated successfully, but these errors were encountered: