Live data from Hacker News

Recursion is lying to you

blog.gaborkoos.com

31–40 of 51 posts

Re: Recursion is lying to you

#32

Most of these issues are a consequence of recursion never getting the same codification as the rest of the jmp patterns we eventually turned into control structures - eg: if, for, while, try/catch. In the meantime, the theory of structured recursion[recursion schemes] has been developing, yet no language offers then as first class constructs. The best we get is library support. Imagine if we had to import a package t…

I might not be understanding what you’re saying here… but recursion compared to your if/whiles, are inherently coupled to the shape of the type they traverse where in the prior two we define data comparisons or input sizes.

Using a jmp isn’t really a call as you’d know, and information is lost that would be crucial to unwinding a recursion.

A recurse keyword would still leave the person writing the code with the decision with proving termination with base cases or trampolines — lest there is just a bunch of math that would unwind your recursion into a better bounded problem. Which is kind of what current keywords do anyway.

Re: Recursion is lying to you

#34
post #29

Earlier quoted context omitted.

You’ve never read a 5000 line recursive method that returns different numbers of arguments depending on where it chose to execute the recursive call.

If all you do is replace the recursion in that example with one (or more) loops with mutable indexes, chances are the code will get worse .

Oh don’t worry it had more than a half dozen of those nested as well

Re: Recursion is lying to you

#35
post #14
post #8

A fun quote from the article, discussing a basic Fibonacci recursive implementation: > Each call branches into two more calls, so the total number of calls grows as O(2ⁿ). Well, no, not really. If anyone bothers counting how many recursive calls are actually made, the result is far from powers of two: n | result | # of calls 1 | 1 | 1 2 | 1 | 3 3 | 2 | 5 4 | 3 | 9 5 | 5 | 15 6 | 8 | 25 7 | 13 | 41 8 | 21 | 67 9 | 34…

Really it's worse than exponential, because the size of the input is not n, it's the number of bits needed to store n i.e. log n. So as the number of bits k grow, it's growing phi^(2^k).

Big O analysis never implies size in bits. It's just often done. In this case, n is just the numerical value of the input, so I don't think this is correct.

Re: Recursion is lying to you

#36
post #25

Earlier quoted context omitted.

maybe the wrong terminology, but non-tail-call-optimized recursions spray their state across space with each iteration consuming a new stack frame, where iterative algorithms live in one stack frame and can re-use temporaries. the state spraying results in consumption and spilling down the memory hierarchy, from registers through caches. i think of this as the memory hierarchy being designed to best perform when spat…

> maybe the wrong terminology, but non-tail-call-optimized recursions spray their state across space with each iteration consuming a new stack frame, where iterative algorithms live in one stack frame and can re-use temporaries If that's what you mean then I'm afraid it sounds like you're mixing a few things up. For example, imagine depth-first search: you're going to need a stack somewhere, whether it's the CPU stac…

let me try arguing it this way: it is impossible for a non-tail-call-optimized recursion to use constant space. the stack grows with O(depth) and therefore memory usage does as well. a consequence of this is that the finite storage lru caches and registers are polluted with one-time-use variables which reduces their availability for actually useful caching.

it IS possible for a loop to use constant space. pre-allocate temporaries and inline any function calls. everything lives in one stack frame. simply iterating the loop itself does not come with fixed space overheads from allocating new stack frames.

i suppose one thing i should mention, i am thinking of extreme optimization use cases where the entire data structure fits in cache (or close to it). think like in-cache tries or similar. if you're hitting main memory with each iteration anyway, it doesn't really matter.

Re: Recursion is lying to you

#37

Most of these issues are a consequence of recursion never getting the same codification as the rest of the jmp patterns we eventually turned into control structures - eg: if, for, while, try/catch. In the meantime, the theory of structured recursion[recursion schemes] has been developing, yet no language offers then as first class constructs. The best we get is library support. Imagine if we had to import a package t…

I might not be understanding what you’re saying here… but recursion compared to your if/whiles, are inherently coupled to the shape of the type they traverse where in the prior two we define data comparisons or input sizes. Using a jmp isn’t really a call as you’d know, and information is lost that would be crucial to unwinding a recursion. A recurse keyword would still leave the person writing the code with the deci…

You're absolutely right. Recursion is tied to the shape of the data being recursed. It can be expressed a number of ways, but generally having a functor for the data type gives a lot of milage in operating over it. See https://github.com/passy/awesome-recursion-schemes for more of what I'm getting after.

Re: Recursion is lying to you

#38
post #36

Earlier quoted context omitted.

> maybe the wrong terminology, but non-tail-call-optimized recursions spray their state across space with each iteration consuming a new stack frame, where iterative algorithms live in one stack frame and can re-use temporaries If that's what you mean then I'm afraid it sounds like you're mixing a few things up. For example, imagine depth-first search: you're going to need a stack somewhere, whether it's the CPU stac…

let me try arguing it this way: it is impossible for a non-tail-call-optimized recursion to use constant space. the stack grows with O(depth) and therefore memory usage does as well. a consequence of this is that the finite storage lru caches and registers are polluted with one-time-use variables which reduces their availability for actually useful caching. it IS possible for a loop to use constant space. pre-allocat…

> let me try arguing it this way: it is impossible for a non-tail-call-optimized recursion to use constant space. [...]

What you're really saying here is that if you have a non-tail-call-optimizing compiler (i.e. if your compiler and/or language suck at optimizing recursion), and your algorithm uses enough stack space that the resulting cache pollution affects the performance (absolutely not every algorithm falls in this bucket!), then recursion is likely (not certain!) to be slower than recursion.

I'm sure you know that even your first assumption immediately fails for all the (many) tools that handle tail-recursion just fine. Isn't it then a gross overgeneralization to just claim "iterative algorithms are faster than recursive ones", as if your situation is universally representative of everyone's?

> i suppose one thing i should mention, i am thinking of extreme optimization use cases where the entire data structure fits in cache (or close to it). think like in-cache tries or similar. if you're hitting main memory with each iteration anyway, it doesn't really matter.

FWIW, the scenario you're imagining is even more niche than that. You're assuming a case where, for example, the hardware prefetcher isn't able to predict (or doesn't have enough bandwidth to) fetch the next cache line before you need it. You're also assuming the data structure is large enough (or your access pattern unlucky enough) that cache misses are actually affecting you -- i.e. not just "fits in cache", but takes up most of the room, too. You're also assuming that compilers are equally good at optimizing recursion and iteration (aside from tail-recursion), which is also not true -- for example, large functions tend to get optimized more poorly than smaller ones (including more stack accesses!), and your iteration is much more likely result in large functions being generated.

Are there situations in which your assumptions hold? Definitely. Are you way, way overgeneralizing? Sure looks like that too.

Re: Recursion is lying to you

#39
post #5
post #2

CS 101, no? Recursion is easier to write, but less performant and more risky than iteration.

Risky?

I once wrote a Xbox 360 indie game that would crash only in unusual situations due to stack overflow. The game involved essentially gluing together balls, and it would detect if the balls surrounded something. I used recursive DFS to check connectivity of the balls. And most of the time, there would be a couple dozen balls max connected. But you could definitely intentionally connect many many more balls if you wanted to. At a certain point, it would crash with a stack overflow. Changing it to not use recursion (explicit data structures) fixed the crash.

Re: Recursion is lying to you

#40
post #36

Earlier quoted context omitted.

let me try arguing it this way: it is impossible for a non-tail-call-optimized recursion to use constant space. the stack grows with O(depth) and therefore memory usage does as well. a consequence of this is that the finite storage lru caches and registers are polluted with one-time-use variables which reduces their availability for actually useful caching. it IS possible for a loop to use constant space. pre-allocat…

> let me try arguing it this way: it is impossible for a non-tail-call-optimized recursion to use constant space. [...] What you're really saying here is that if you have a non-tail-call-optimizing compiler (i.e. if your compiler and/or language suck at optimizing recursion), and your algorithm uses enough stack space that the resulting cache pollution affects the performance (absolutely not every algorithm falls in…

I'm not the OP, but I think he's saying something much simpler than that. Typically with a recursive algorithm, the state you need to push is tiny. Usually a word or two. But often you need a lot of temporaries to calculate that state.

If you use recursion, both the absolutely necessary state and the temporaries are pushed onto the stack (along with the stack frame the language runtime wants). If you use iteration, the programmer just pushes the necessary state onto the stack, and reuses the temporaries in place.

The rest of his claims follow from those facts. Space consumption is less because you are saving less. Cache locality is better both because the temporaries aren't scattered across the stack, and because you are reusing the same locations over and over again.

The downside of iterative solutions is stacks are more efficient memory allocators than heap algorithms - especially if you are forced to grow the array your pushing things onto many times. But if you know that in advance so you can allocate the full amount up front, then at the limit when N -> ∞ iteration will always win over recursion.

Post reply on HN