Tail Call Optimization: The Musical (2019) [video]
youtube.com
youtube.com
Sadly, these days Safari is the only engine that does TCO. It also keep track of a decent number of frames (currently 128). For more details on the implementation (called Shadow Chicken), see https://webkit.org/blog/6240/ecmascript-6-proper-tail-calls-....
Aside, calling tail calls TCO makes it sound like it is just an optimization, which it is not. Tail calls enable beautiful control flow inside of state machines. It is a new fundamental capability and not an optimization.
I was nervous, since I am not used to singing like that (I am a professional bassoon player, working full time in a symphony orchestra). The risk of me bombing was rather small. This thing is different in just about every way.
Would I stand up and sing in a tech conference? Not without vomiting before going on stage (not sure whether that is an exaggeration for comic effect). It requires more than just a little chutzpah.
First example that comes to mind is the Tower of Hanoi algorithm:
def toh(ndisk, source, via, target):
if ndisk > 0:
toh(ndisk - 1, source, target, via)
target.append(source.pop())
toh(ndisk - 1, via, source, target)
The function calls itself 2 times. Same thing is the fibonacci function which relies on the 2 previous results.Here, you'll certainly need memoization and other techniques to not blow up the stack.
[1] - https://en.wikipedia.org/wiki/Continuation-passing_style
It's an optimization that effectively turns non-working code into working code, in ways that aren't always obvious or predictable and possibly in different ways across different compilers/interpreters.
I'd rather the code blow up in my face immediately than spend time wondering why it gets optimized on engine X and not on engine Y, or why seemingly trivial code changes cause the function to have drastically different runtime behavior.
Clojure did this right. No TCO, loop/recur construct instead.
As a side note at the cost of being snarky, it's also probably a good thing that developers who get overexcited about recursion are being encouraged to use proper functional idioms instead of abusing it.
I'll side with the Python teaching that explicit is better than implicit. If the code says the stack will blow, then the compiler/runtime should not try to avoid that.
Your comment piqued my curiosity and I made a little experiment :)
The following function:
int rec(int n) {
if (n < 1) return 0;
return 1 + rec(n - 1);
}
will get TCOd by gcc but not by clang (!!)I don't know, probably I'm not smart enough for TCO, but I just don't want to work like that. It's a minefield.
rec: // @rec
bic w0, w0, w0, asr #31
ret
That's taking the sign bit, replicating it to 32b and then using it as a not'd mask. negative numbers:
w0, asr #31 => -1
bic w0, w0, -1 => and w0, w0, 0 => ret 0
positive numbers:
w0, asr #31 => 0
bic w0, w0, 0 => and w0, w0, 0xffffffff => ret w0
gcc is also pretty good [2]: rec:
cmp w0, 0
csel w0, w0, wzr, gt
ret
[1] https://godbolt.org/z/q9hM7z int count(int n) {
int res = 0;
for (int i=0; i < n; i++) res++;
return res;
}In languages which do have guaranteed TCE like scheme or ML variants, I think I would rather see some way to explicitly say “this is a tail call which should be eliminated” or “all recursive calls to this function should be tail calls” and then have the compiler complain if you have a call which should be eliminated but isn’t in tail position. I think the reason for clojure not doing TCE is that, in general[1], it is a global optimisation (not possible by simple syntax transformation) that the jvm doesn’t do. So clojure didn’t really have a choice in whether or not to have TCE.
[1] it is more possible to transform recursive or mutually recursive code into iterative code, but the latter becomes hard in the face of a dynamic language where functions can be redefined and impossible to do locally for all tail calls.
In many cases PTC causes tail calls to be slower, and it messes up stack traces. This would happen even if a developer doesn't want/need tail calls. So it was a feature that would degrade performance and debuggability of existing websites to allow new code to use a more recursive style of programming.
The main performance problem comes when you have tail calls that need to grow the stack. The engine will need to do work to adjust the stack frame.
In languages like C++, the compiler chooses to do tail calls when it improves performance, but with tail calls as a language feature, engines have no choice in the matter and had to do it even when they would significantly increase call overhead.
On top of this, there there were a number of implementation issues and corner cases. For example, most engines have some extra code layer for marshalling cross-site function calls that couldn't be removed. I also remember there being issues with Windows x64 ABI/stack frames. Tail calls from interpreter into JIT code were another implementation headache.
All this added up to have JS engines basically boycott the feature.