起源是在写webscoket服务的时候,发现开发工具偶尔报Maximum call stack size exceeded
的问题。由于当时没时间,就草草把BUG修复了,并未深究原因。现在复盘工作的时候,又想起这个问题,于是再把这个问题拿出来研究。
出错代码大概是如下:
new Array().push(...Buffer.allocUnsafe(2**17))
一开始看到超出调用栈以为是Buffer的迭代器递归出现了问题,但事实并不是这样。
排查出错原因
由于出错代码是这样的:
/*
* 而且存在临界点,不同机器略有差异
* 本人的机器大于2**17就必报
*/
new Array().push(...Buffer.allocUnsafe(2**17))
当时认为是Buffer的迭代器问题,所以就尝试使用普通数组
/*
* 这段代码在浏览器也报错
* 所以排除nodejs原因,应该是V8造成的
*/
new Array().push(...new Array(2**17).fill(0xff))
发现普通数组也存在类似问题,接下来就是排除迭代器
[...new Array(2**17).fill(0xff)]
发现迭代居然没有异常
,难道是push方法?那就换个方法试试
console.log(...new Array(2**17).fill(0xff))
居然log也报错,那真相只有一个,那就是参数超载
了
那为什么会报超过最大调用堆栈大小,而不是其它错误?
大家都知道,函数再调用函数的时候,是通过存储在调用栈中来保持执行顺序的,而栈是有一定大小,比如递归上数百万次后也会出现爆栈。
那么是否真的是因为栈不够用了?还是说参数对调用栈也存在一些影响?
- 接下来我们就来逐一排查。首先确定是否是真的因为栈不够用了
# 通过调整栈大小,来判断是否是栈耗尽了,stack-size的单位是KB,默认是984
node --stack-size=2048 -e "new Array().push(...Buffer.allocUnsafe(2**17))"
发现果然运行正常,所以可以确定是栈耗尽了
- 排查参数的数量对栈的影响
function recursionDepth(paramLen) {
let deepth = 0;
function f(...paramList) {
deepth++;
Math.random() + f(...paramList); // 防止尾递归优化
}
try{
f(...Buffer.allocUnsafe(paramLen))
} catch (err) {
console.log(`当参数长度为${paramLen},最大深度则为:${deepth}`)
}
}
recursionDepth(2**4)
recursionDepth(2**8)
recursionDepth(2**12)
recursionDepth(2**16)
recursionDepth(2**20)
输出结果:
当参数长度为16,最大深度则为:3489
当参数长度为256,最大深度则为:455
当参数长度为4096,最大深度则为:30
当参数长度为65536,最大深度则为:1
当参数长度为1048576,最大深度则为:0
所以由此确定参数的数量也是需要暂用调用栈的空间,而当参数长度达到足够长,即使1帧也可以压垮整个调用栈,超出调用栈空间。
就不该用push
方法
一楼说得对,本来就不该用push。 写下一行代码应该对对代码有复杂度的粗略估计。 解构也是有迭代耗时的。 解构一个超过10长度的数组本来就值得警惕
======================
参数栈这个如果c学得扎实应该有点概念,以前写c还有入栈顺序,返回值放哪的协议呢 传递1w个指针,💥 不过js解释器聪明点这块说不定能自己优化掉,
楼主问题是把解构出来的值当作函数参数, 那如果有10k个元素就会10k个参数,肯定会爆栈的。 使用解构方法一定得注意这个问题。
还有apply也有类似陷阱: https://github.com/ruanyf/es6tutorial/pull/520
@waitingsong 嗯,我现在思考的是调用栈只是为了保证方法的执行顺序,按照道理来说数据都已经在内存中,根本没必要在调用栈中存参数,所以我认为这是一个设计失误,下次假期抽点时间看看它为什么非要存参数。
@zy445566 我猜测可能是因为解构数组导致函数参数个数不定,于是执行环境无法做某些(针对固定参数个数的)优化。于是…… 要不你试试看把例子改成固定超大个数参数(比如你测试环境下面参数个数),看看是否也爆栈
length 2**17
这么大的数组,感觉还有其他坑在等着
@waitingsong 我觉得apply这个陷阱比解构这个更加值得注意,比较容易忽略和失去警惕性
@AnzerWall 所以过于精妙的奇技淫巧还是少用。
@chenkai0520 我觉得这个跟push没有关系,他问题的关键是解构之后参数数量太多导致栈爆了,如果楼主用for循环,然后每次push时候只只传一个参数应该没有问题
@youth7 对的,即使是console. log也有相同的问题。其实不管是不是解构,只要参数够多,目前爆栈就一定存在。
@zy445566 那是当然,关键原理就是x86的计算模型就是通过寄存器/栈来传递参数,寄存器放不下了就放栈,栈也放不小了就栈爆了
@youth7 学习了,估计V8自己实现了一套,但是基本原理应该不变。