9

I have 2 node js programs.

console.log('TEST A:');
function computeMaxCallStackSizeA() {
    try {
        return 1 + computeMaxCallStackSizeA();
    } catch (e) {
        return 1;
    }
}

for (let i = 0; i < 5; i++)
  console.log(computeMaxCallStackSizeA());
  
console.log('\nTEST B:');

function computeMaxCallStackSizeB() {
    try {
        let a = [];
        for(let i=0;i<100;i++) a.push('1234567890');
        return 1 + computeMaxCallStackSizeB();
    } catch (e) {
        return 1;
    }
}

for (let i = 0; i < 5; i++)
  console.log(computeMaxCallStackSizeB());

  • The first program's result is 12559.
  • The second program's result is 13870.
  • Why does the second program use more memory but still has a larger call stack maximum size?

UPDATE:

  • If we change the program as follow:

function computeMaxCallStackSizeC() {
    try {
        let a1 = '11111111111111111111111111111111111111111111111111111';
        let a2 = '22222222222222222222222222222222222222222222222222222';
        let a3 = '33333333333333333333333333333333333333333333333333333';
        let a4 = '44444444444444444444444444444444444444444444444444444';
        let a5 = '55555555555555555555555555555555555555555555555555555';
        let a6 = '66666666666666666666666666666666666666666666666666666';
        return 1 + computeMaxCallStackSizeC();
    } catch (e) {
        return 1;
    }
}

for (let i = 0; i < 5; i++)
    console.log(computeMaxCallStackSizeC());

The maximum recursion depth is decreased. Because Stack memory is used to store local variables (such as a number, a string) and heap memory is used to store the data of dynamically allocated pointer (such as an array). Is that right?

12
  • 1
    Did you try to run these multiple times, did they always have the same results? I'd expect the OS to provide different amounts of memory at different times.
    – Bergi
    Commented Jul 15, 2020 at 18:09
  • 1
    Aren't objects usually stored on the heap?
    – MinusFour
    Commented Jul 15, 2020 at 18:13
  • 1
    OP, I edited this into a code snippet, please roll back or adjust as you'd like.
    – Lewis
    Commented Jul 15, 2020 at 18:18
  • 1
    I've tagged this [v8], hoping @jmrk takes a look at it and will answer with entertaining details about the stack implementation and why this estimation code is flawed :-)
    – Bergi
    Commented Jul 15, 2020 at 18:29
  • 1
    @Christian: that "12475" number already contains mostly optimized frames, that's why it's so close to 12564. Unoptimized you'd see ~9600 or thereabouts.
    – jmrk
    Commented Jul 15, 2020 at 19:39

1 Answer 1

7

My guess would be that the function gets optimised earlier in the second program than in the first because it does more work, and that the optimised function needs less stack space. -- @Bergi

Correct. The presence of the loop means that the function does more work per invocation, and that causes it to get optimized after fewer invocations. Optimizing a function generally results in it using a different stack frame size. I don't think there's a general rule about the relative sizes of these stack frames; apparently in this case the optimized version of the function uses a smaller stack frame, and I assume that's the vastly more common case, but there might well be counter-examples where it's the other way round.

Once the first function (A) gets optimized, since it does less stuff it also uses less stack space than B, so reaches a higher maximum recursion depth.

With JavaScript being as dynamic as it is and modern engines collecting type feedback and using that for decision-making during compilation, you may also well see the same function take different amounts of stack space on different times that it gets optimized -- e.g. when you use a library/helper function in two different apps, or when you call it with other inputs, etc.

Also, regardless of optimized compilation, stack frame sizes (and hence maximum recursion depths) can and do change depending on:

  • which hardware you're running on (since machine code is, by definition, specific to a given CPU architecture),
  • which engine you're using (V8/SpiderMonkey/JavaScriptCore/etc...),
  • which version of that engine you're using (as its developers make changes to such internal details of how it operates),
  • potentially which embedder (say, Chrome or Node) you're using, even if they use the exact same version of the same engine,
  • and potentially which operating system you're running on.

So it is not advisable to rely on a particular value.

Why does the second program use more memory but still has a larger call stack maximum size?

The array is allocated on the heap, so the size of the array doesn't affect the function's stack frame size. Observe how if you replace "100" with "10", the result is exactly the same.

It's hard to give an accurate yet intuitive idea of what consumes stack space. It boils down to "local values that have to be kept around", which sometimes corresponds to local variables, unless the compiler can get away with not allocating a stack slot for a local variable (and keeping its value in a register instead). Also, often there are additional "internally used" slots to hold temporary values that you don't directly see in the JavaScript code, especially for more complicated functions, where e.g. an object property might be read only once, but is used twice, and in the meantime is stored in a stack slot. Or when growing an array (as a.push(...) does), which is a fairly complex operation under the hood and as such causes a bunch of temporary values to be stored on the stack.

How to estimate javascript call stack maximum size?

Under the hood, at least in V8, the limit is a little less than a megabyte. In terms of JavaScript function calls, a decent rule of thumb is:

"A few hundred calls, or a few thousand if your functions are tiny."

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.