如何估计 javascript 调用堆栈的最大大小?

How to estimate javascript call stack maximum size?

我有 2 个节点 js 程序。

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());

更新:

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());

减少最大递归深度。因为栈内存用来存放局部变量(比如数字,字符串),堆内存用来存放动态分配的指针(比如数组)的数据。是吗?

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

正确。循环的存在意味着函数每次调用做更多的工作,这导致它在更少的调用后得到优化。优化函数通常会导致它使用不同的堆栈帧大小。我认为没有关于这些堆栈帧的相对大小的一般规则;显然,在这种情况下,该函数的优化版本使用了较小的堆栈框架,我认为这是更常见的情况,但可能会有相反的例子。

一旦第一个函数 (A) 得到优化,由于它做的事情更少,它使用的堆栈也比 B 少 space,因此达到更高的最大递归深度。

由于 JavaScript 是动态的,现代引擎收集类型反馈并在编译期间将其用于决策制定,您可能还会看到相同的函数占用不同数量的堆栈 space 在不同的时间进行优化——例如当您在两个不同的应用程序中使用 library/helper 函数时,或者当您使用其他输入调用它时,等等

此外,无论优化编译如何,堆栈帧大小(以及最大递归深度)都可以并且确实会根据以下因素发生变化:

  • 您 运行 使用的硬件(因为根据定义,机器代码特定于给定的 CPU 架构),
  • 您正在使用哪个引擎 (V8/SpiderMonkey/JavaScriptCore/etc...),
  • 您正在使用该引擎的哪个版本(因为它的开发人员对其运行方式的此类内部细节进行了更改),
  • 您可能正在使用哪个嵌入器(例如,Chrome 或 Node),即使它们使用同一引擎的完全相同版本,
  • 以及您 运行 可能使用的操作系统。

因此不建议依赖特定值。

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

数组分配在堆上,因此数组的大小不会影响函数的栈帧大小。观察如果将“100”替换为“10”,结果是完全一样的。

很难准确而直观地了解什么消耗了堆栈 space。它归结为“必须保留的局部值”,有时对应于局部变量,除非编译器可以不为局部变量分配堆栈槽(而是将其值保存在寄存器中)。此外,通常还有额外的“内部使用”插槽来保存您在 JavaScript 代码中不会直接看到的临时值,特别是对于更复杂的函数,例如一个对象 属性 可能只被读取一次,但被使用了两次,同时被存储在堆栈槽中。或者在增长数组时(如 a.push(...) 所做的那样),这是一个相当复杂的底层操作,因此会导致一堆临时值存储在堆栈中。

How to estimate javascript call stack maximum size?

在幕后,至少在 V8 中,限制略小于 1 兆字节。在 JavaScript 函数调用方面,一个不错的经验法则是:

“几百个调用,或者几千个,如果你的函数很小。”