7

I've found the following question here:

The following recursive code will cause a stack overflow if the array list is too large. How can you fix this and still retain the recursive pattern?

And the answer:

The potential stack overflow can be avoided by modifying the nextListItem function as follows:

var list = readHugeList();

var nextListItem = function() {
    var item = list.pop();

    if (item) {
        // process the list item...
        setTimeout( nextListItem, 0);
    }
};

The stack overflow is eliminated because the event loop handles the recursion, not the call stack. When nextListItem runs, if item is not null, the timeout function (nextListItem) is pushed to the event queue and the function exits, thereby leaving the call stack clear. When the event queue runs its timed-out event, the next item is processed and a timer is set to again invoke nextListItem. Accordingly, the method is processed from start to finish without a direct recursive call, so the call stack remains clear, regardless of the number of iterations.

Can somebody please explain to me:

  1. whether this use case is ever practical
  2. why long array can cause stack overflow
Max Koretskyi
  • 101,079
  • 60
  • 333
  • 488
  • It's worth noting that `setTimeout(fn, 0)` doesn't work as expected: in most browsers, the minimum delay is 4ms. – lonesomeday Mar 25 '16 at 08:02
  • @lonesomeday, yeah, I know that, thanks. What about the questions I asked in the end? :) – Max Koretskyi Mar 25 '16 at 08:06
  • As to me, the given explanation is quite clear. If you can't grasp it, you probably have to start from [the basics](https://en.wikipedia.org/wiki/Call_stack). – hindmost Mar 25 '16 at 08:10

3 Answers3

9

This is just a hacky alternative to trampolines, which in turn are just a hacky alternative to TCO.

When you call a function in Javascript, you add a frame to the call stack. That frame contains information about the variables in the scope of the function and how it was called.

Before we call a function, the call stack is empty.

-------

If we call function foo, then we add a new frame to the top of the stack.

| foo |
-------

When foo finishes executing, we pop the frame off the stack again, leaving it empty again.

Now, if foo in turn calls another function bar, then we'll need to add a new frame onto the stack, whilst foo is executing.

| bar |
| foo |
-------

Hopefully you can see that if a function calls itself recursively it keeps adding new frames to the top of the call stack.

| ...          |
| nextListItem |
| nextListItem |
| nextListItem |
| nextListItem |
----------------

Recursive functions will keep adding frames until either they finish processing, or they exceed the max length of the call stack, resulting in an overflow.

Because setTimeout is an asynchronous operation, it doesn't block your function, which means nextListItem will be allowed to finish and its frame can be popped off the call stack—preventing it from growing. The recursive call will be handled with the event loop instead.

Is this pattern ever useful? The max size for the call stack depends on your browser, but it can be as low as 1130. If you wanted to process an array with a few thousand elements using a recursive function, then you'd risk blowing the call stack.

Trampolines use a similar technique, but rather than offloading the work to the event loop, you return a function which calls the next iteration instead, then the calls can be managed with a while loop (which doesn't affect the stack).

var nextListItem = function() {
  var item = list.pop();

  if (item) {
    // process the list item...
    return nextListItem;
  }
};

while(recur = recur()) {}
Community
  • 1
  • 1
Dan Prince
  • 29,491
  • 13
  • 89
  • 120
  • Thanks, So is call stack length really just limited by the number of function calls, and not the memory consumption of each function? – Max Koretskyi Mar 25 '16 at 09:18
  • 1
    Yeah, generally the call stack would be represented with fixed size array of N slots for stack frames. If you try and put something in slot N + 1, then you'll get an overflow. – Dan Prince Mar 25 '16 at 09:29
  • How interesting, thanks) And it also has nothing to do with stack as in stack/heap memory allocation? – Max Koretskyi Mar 25 '16 at 10:04
  • The stack in stack/heap _is_ the call stack. – Dan Prince Mar 25 '16 at 10:12
  • Do you know a good article to read about that, now wikipedia? – Max Koretskyi Mar 25 '16 at 10:33
  • 1
    There's [a good MDN article](https://developer.mozilla.org/en/docs/Web/JavaScript/EventLoop) which explains the stack, heap and event loop together. – Dan Prince Mar 25 '16 at 10:48
  • I've checked the article, it's what I already know. I meant the other stack, [this one](http://gribblelab.org/CBootcamp/7_Memory_Stack_vs_Heap.html) – Max Koretskyi Mar 25 '16 at 12:25
  • The stack in that article _is_ the call stack. _"What is the stack? It's a special region of your computer's memory that stores temporary __variables created by each function__"_. – Dan Prince Mar 25 '16 at 15:58
  • so what you're saying we're talking about the same stack, right? – Max Koretskyi Mar 25 '16 at 16:21
  • 2
    Yeah. Generally in programming when you hear the phrase _"a stack"_ it refers to an instance of the stack data structure, when you hear _"the stack"_ it refers to the call stack, which features in most programming languages. – Dan Prince Mar 25 '16 at 16:49
  • I see, appreciate) One clarification though, the stack is limited then by the count of running functions and by the memory size each running function occupies for its local variables? – Max Koretskyi Mar 25 '16 at 17:13
  • The stack has a size limit (number of frames). The stack doesn't care how many variables you keep in each frame, that's handled by random access memory. – Dan Prince Mar 25 '16 at 17:18
  • Hmm, but the article states that _Another feature of the stack to keep in mind, is that there is a limit (varies with OS) on the size of variables that can be store on the stack. This is not the case for variables allocated on the heap._ what is meant by that? – Max Koretskyi Mar 26 '16 at 06:17
  • If you are using Chrome devtools to find out if you managed to not make the call stack to overflow, please keep in mind that the devtools will remember the call stack even with a setTimeout function for as long as the devtools window is open. – Jahrenski Oct 21 '19 at 14:35
2
  1. It normally isn't, but in the off chance you decide you need to recursively chain the same function call for long sequences this could come in handy.

  2. Stack overflow during recursive operations occurs when the amount of stack memory allocated for a particular program has been fully utilized. A sufficiently long array that is traversed recursively can cause a stack overflow. Perhaps you do not understand how the call stack works?

hofan41
  • 1,438
  • 1
  • 11
  • 25
1

The repeating for loop is the most efficient for the code posted. However, if your actual code cannot be fitted to using a for loop, then there is another alternative.

The use of setTimeout depends on your opinion of 'practical,' so let's just list the facts so you can decide for yourself.

  • setTimeout forces the browser to swamp the CPU with operations to get millisecond precision timing. This can be very power inefficient.
  • With setTimeout, every iteration will cost 4ms. Just 4 iterations will take the time for a whole frame to get rendered. 250 iterations, and a whole second goes by.

But there is an alternative to setTimeout that will help you achieve exactly what you are trying to do without using setTimeout: the DeferStackJS library. If you use DeferStackJS, then all you would need to do is as follows.

var list = readHugeList();

var nextListItem = function() {
    var item = list.pop();

    if (item) {
        // process the list item...
        DeferStack( nextListItem );
    }
};

Please emphasize that the above snippet of code is intended for demonstrating how to integrate DeferStackJS. Truth be told, using either DeferStackJS or Array.prototype.pop would be very inappropriate for this specific snippet of code. Instead, the following code would beat both of them hands down.

var list = readHugeList();

var nextListItem = function() {
    "use strict";
    var item, i = list.length;
    while (i--) { // Bounds check: as soon as i === -1, the while loop will stop.
                  // This is because i-- decrements i, returning the previous
                  // value of i
        item = list[i];
        if (!item) break; // break as soon as it finds a falsey item 
        // Place code here to be executed if it found a truthy value:

        // process the list item...
    }
    if (i !== -1) {
        // Place code here to be executed if it found a falsey value:

    }
};

The only reason DeferStackJS is mentioned is because I am a firm believer in that the #1 duty of a person answering a forum is to answer the original question asked. Then after they answer that they can put up commentary second-guessing the question about what was intended to be asked like this.

Jack G
  • 4,553
  • 2
  • 41
  • 50