PragTob
I wrote up a detailed blog post about tail call optimization in Elixir/Erlang and its performance. The TLDR; sort of is that none tail call optimized recursive functions (body-recursive) can be faster and more memory efficient than TCO functions. This is something that I never thought before, that TCO is always faster seems to be a common misconception. My example is also not contrived - it’s an implementation of map.
Posting here to share as I think it’s important knowledge
Also to get feedback, maybe someone has another implementation that blows mine out of the water.
PS: Hope this is the appropriate forum/tag?
Trending in Discussions
As the title says, please share what you’ve been up to with Elixir. Whether that’s been learning it, looking into it, making stuff with i...
New
Hey there,
It’s been more than a year since we started using LiveView as our main UI library and building a whole library of UI componen...
New
I am happy to introduce the very α version of the new programming language compiled to BEAM.
Welcome Cure.
It has literally three kille...
New
Quite interesting article Google brought me. Didn’t find any mentions about it here.
What do you think in general? Would you use togethe...
New
Hi everyone!
The first release candidate for the Expert language server project is now available!
We’ve published a press release detai...
New
Since we have deprecated our Erlang sections (as we have dedicated Erlang Forums now) let’s add this thread for those who’d like to post ...
New
:warning: Security advisory: Decimal DoS vulnerability
A vulnerability has been published for decimal where very large exponents can cau...
New
Other Trending Topics
Hi there! We created Gust: A task orchestrator inspired by Airflow.
For those who have never heard about Aiflow, it’s a Python-based wor...
New
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
With AI doing more of the implementation work, I’ve been wondering how much coding I should deliberately keep doing myself.
My main conc...
New
It’s not that it’s vocabulary is too advanced. It’s something worse.
I get lost trying to follow even a paragraph written by Claude. It’...
New
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #library
- #deployment
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #elixirconf
- #channels
- #exunit
- #discussion
- #code-sync
- #podcasts
- #javascript
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixirconf-us
- #ai
- #blog-post
- #elixir-ls
- #phoenix_html
- #iex
- #graphql
- #genstage
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #metaprogramming
- #hex
- #security










Showing Posts 1 to 10- Show Best Posts
- Show All (oldest first)
- Show All (newest first)
NobbZ
Some time ago I read something, I thik it was written by @rvirding but I’m not really sure about it, that aimed as well at this discussion.
It TL;DR was roughly: When building a list which order matters, it does not really matter if you build it “on the stack” or in an accumulator which you reverse afterwards. One of both might kill the stack, the other one just will stress your GC. But whenever order of the resulting list does not matter anymore or when you reduce to a single value, an accumulater in a tail recursive function is way better, because it is faster and does not stress the stack that much.
Also I do have an important nitpick about nomenclature:
We don’t write TCO’d functions. Tail Call Optimisation is an compile time optimisation done by a compiler. This particular optimisation does mean to rewrite a tail recursive function in a way that it can get compiled into a loop/direct jump in the targetted language instead of a function call. Sice we don’t push and pop on a/the stack when looping we save up some cycles there, as well as we do not need to grow the stack by 20k frames which are more or less the same.
There are some discussions around about the fact if TCO is really a good thing so in many compilers (not necessarily for erlang or elixir) you can switch off TCO (as well as other optimisations). Some languages (-> Python) do even refuse to implement TCO in the referential implementation because of these concerns: When looping instead of calling, then there are steps missing in the stacktrace and you can’t know how deep you actually are before getting that exception thrown in the face.
rvirding
Sorry, there are some cases where TCO functions are absolutely critical. Kill your system and make it worthless if you don’t do it critical. You just have to get it right.
The examples you show are all calls to functions which (eventually) return and in those cases TCO is a nice but not need. It may not save you heap but it will save you stack, which can actually save you GC as well[*].
No, the main case where is TCO is critical is in process top-loops. These functions never return (unless the process dies) so they will build up stack never to release it. Here you have to get it right. There are no alternatives. The same applies if you top-loop is actually composed of a set of mutually calling functions. There there are no alternatives. Sorry for pushing this again, and again, but it is critical.
In other cases my way of choosing is the one which gives me the most intelligible code. And that varies depending on what the function is supposed to do and how I choose to do it.
Robert
[*] This is because the heap and stack share the same memory area and when it is full you get a gc. So keeping the stack small can delay gc.
PragTob
Hey, thanks for pointing that out and the nitpick about nomenclature. You are right, I tend to mix the two as I always think about it as “a function to which tco will apply” - I’ll update the blog post to make the distinction between TCO and tail-recursive functions clearer.
Interesting to hear about Python, never thought any language would refuse to implement it.
PragTob
totally agreed, of course for some cases TCO is super critical. If I recall correctly that was the case for OTP servers.I’ll also see that I incorporate you comment into the blog post. Thank you for taking the time to comment, it’s so great to have you around here!
Qqwy
A while back, I had a discussion on Github with @josevalim and other members of the Elixir core group, as I wanted to add “this function is tail-recursive” to
List.foldl/3, and an “this function is not tail-recursive” toList.foldr/3.In the end it was not added as people argued that the difference was not important enough.
Coming from a Haskell background, I am still not fully convinced as in Haskell, tail-call optimization is extremely important, since you are dealing with lazily-evaluated infinite lists most of the time.
EDIT: Actually, I just found out about ‘Tail recursion modulo cons’ which basically lets languages that implement it treat the ‘cons’ operator (the operator that is used to add something to the front of a list) as something that does not prevent the rest of the function from being tail-recursive.
Qqwy
I think that actually the ‘tail recursion modulo cons’ is one of the things that creates the misconception of tail-recursion being fast.
In Haskell, the (tail-recursive!) map is written as follows (This is the implementation of
mapfrom the Prelude, Haskell’s standard library):That’s right! It is written exactly the same (*) as your non-tail-recursive
mapvariant in Elixir:Note that it is not at all required in Haskell’s tail-recursive version to reverse the list at the end. This reversal is probably where quite a lot of time is spent in your tail-recursive map (you basically have to iterate over the list a second time), besides the memory requirement for reverse that you have mentioned.
So, although it is still critical in the top-loops of processes, as @rvirding has mentioned, I think that Tail-recursion becomes a whole lot less useful when you’re not allowed to construct lists without breaking it.
(*) (bar the parameter order – Haskell likes the list parameter at the end because of currying, while Elixir likes it at the front because of pipelines)
michalmuskala
As far as I understand how lists work in Elixir/Erlang it also has the “tail recursion modulo cons” optimisation. That’s why a map that builds up a list on the stack is also tail recursive in Elixir/Erlang.
After http://erlang.org/doc/efficiency_guide/myths.html:
With that in mind both example functions end up being tail recursive (and thus faster), it’s just that one leverages an additional compiler optimisation do to that, so the fact that the function is tail recursive is hidden from the programmer.
Qqwy
Hmm… Now I am not sure any more if any of the conclusions of Erlang’s Tail Recursion is Not a Silver Bullet are actually true.
I haven’t been able to find patch notes for Erlang 12B so far that support or reject this claim. (the section on the Erlang Myths page is not very clear; It is possible that it refers to a different kind of optimization.)
EDIT: I did find this interesting Erlang forum thread, which pointed me to the List Handling part of the Erlang doc’s efficiency section, which, however, states more or less the same as what is included in the Myths section, albeit with a clearer example.
The last post in that forum thread states:
So this seems to refute the idea that the optimization made in R12B is tail-recursion modulo cons.
However, a few posts before, it is stated:
So, because the heap and the stack are in a shared memory space, it seems that the difference is not that huge, and rather in favour of the body-recursive variant.
I am thorougly confused by what all these different sources claim.
Onor.io
I think considering TCO from the stand point of performance is possibly missing the point anyway. We can’t shut off our brains and say “Always use TCO” or “Never use TCO”. If there’s a possibility that the data set you’re processing is large enough that it might blow the stack then use TCO. If TCO isn’t much harder to read or understand than the other way, then use TCO (or don’t). If using TCO will make the code significantly harder to comprehend then I’d really need to see that there’s truly a chance that I will blow the stack.
I find it a little depressing (but not surprising) that so many software developers want easy answers. “Always do a” ;“Never do a” . Life just isn’t that simple or clearcut. Annoying but true.
Qqwy
As for ‘blowing the stack’. It seems that Erlang is very particular in that the stack and the heap are in the same memory space and grow towards each other; so using a lot of one or a lot of the other is okay (but not using a lot of both).
This is very true. There is no ‘universal tool’ in programming. Or life.
It can be annoying at times, but it definitely makes the world a whole lot more interesting.