← Back to context

Comment by a-dub

7 hours ago

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 spatial locality of memory usage is maintained, but it's slightly different from what most people mean when they discuss spatial locality... maybe "cache efficiency" is the better term?

> 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 stack which you use via recursion, or an explicit stack you use via iteration. Iterating doesn't magically remove your need for that space and somehow collapse everything down to one stack frame. And you can reuse temporaries from the heap too, etc.

Fundamentally, there is the question of how much space you need for given algorithm, the question of what algorithm you should use in the first place, the question of whether that particular algorithm should be implemented recursively or iteratively, and the question of what is more maintainable and easier to evolve in practice.

These are all separate questions, but you're conflating them. If your iteration uses constant space but your recursion doesn't, that's because you're not implementing the same algorithm. You're implementing a different algorithm that achieves the same original goal you had. Of course one algorithm might beat the other, that's no surprise.

  • 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.

    • > 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.

      1 reply →