JavaScript中循环与CPS循环的诡异性能表现探究
哇,这个测试结果确实有点颠覆预期,我来帮你拆解几个关键原因:
1. Firefox引擎的尾调用优化(TCO)救了朴素函数循环
你担心朴素函数调用会导致栈空间线性增长,但Firefox的SpiderMonkey引擎对尾递归调用有非常成熟的优化。看你的NonTrampLoop代码:
n1里最后调用n3(i)或n2(i),都是尾调用(函数最后一步就是调用另一个函数,没有后续操作)n2最后调用n3(i),n3最后调用n1(i),全都是尾调用结构
引擎会把这种尾递归转换成类似goto的跳转,完全不会增长调用栈,相当于把递归变成了隐式的循环。这种情况下,朴素函数循环的性能自然不会差,甚至因为引擎对这种规整的函数调用序列优化更到位,表现超出预期。
2. 蹦床循环的优化空间比你想象的大
你的TrampolineStreamlined版本用了一个共享的对象th来传递状态,而不是每次创建新的匿名函数:
- 避免了频繁的堆上函数对象分配和垃圾回收
- 引擎可以把
th的属性访问优化成局部变量(比如var i = th.i),减少属性查找开销
相比之下,常规的for循环虽然直观,但引擎需要处理循环变量的边界检查、增量操作的各种情况;而蹦床的结构更规整,引擎更容易做内联优化,甚至把整个蹦床循环转换成无开销的跳转,所以在Firefox里反而跑的最快。
至于TrampolineSimplistic版本,虽然每次返回匿名函数,但Firefox的引擎可能对这些短生命周期的函数做了逃逸分析,把它们分配在栈上而不是堆上,减少了垃圾回收的压力,所以性能也没拉胯。
3. while(true)循环的优化瓶颈
你写的WhileTrueLoop用了while(true)加break的结构,这种写法反而给引擎的循环优化制造了麻烦:
- 循环条件是常量
true,引擎很难预测循环的退出时机,无法提前做循环展开、边界检查消除等优化 - 里面的
continue和break跳转逻辑比较零散,破坏了循环的规整性,引擎无法把循环转换成更高效的形式
而标准for循环的结构非常清晰,引擎可以轻松识别出这是一个固定次数的循环,直接做循环展开、常量折叠等优化,所以性能反而比while(true)好很多。
4. 测试代码的潜在影响
虽然所有测试都调用了console.log,但不同循环中console.log的调用时机和上下文可能影响引擎的优化。不过因为所有测试都包含相同的console.log逻辑,这部分的影响应该是相对一致的,不是导致结果反常的核心原因。
总结一下:不是你的测试有大疏漏,而是Firefox的引擎对尾递归和规整的函数调用结构优化太激进,反而让CPS风格的循环跑出了比常规循环更好的性能。
内容的提问来源于stack exchange,提问作者Nick

