我知道你可以使用数组作为先进先出的队列来重写递归函数,从而使用简单循环。我听说这样做可以减少堆栈溢出的可能性。
但是如果堆栈溢出不是问题(因为你没有深度递归),是否有理由更喜欢迭代而不是递归?它会更快吗?
我主要关注V8上的JavaScript。
我知道你可以使用数组作为先进先出的队列来重写递归函数,从而使用简单循环。我听说这样做可以减少堆栈溢出的可能性。
但是如果堆栈溢出不是问题(因为你没有深度递归),是否有理由更喜欢迭代而不是递归?它会更快吗?
我主要关注V8上的JavaScript。
递归可能会更快地失败,因为无限递归函数将会耗尽堆栈并产生异常,程序可以从异常中恢复,而迭代解决方案将一直运行直到被外部代理停止。
对于需要时间生成有效输出的代码,递归的主要成本是函数调用开销。迭代解决方案根本没有这个问题,因此在不显式优化递归的语言中,往往在性能关键代码中胜出。
在基准测试中确实很明显,但除非你正在编写性能关键代码,否则你的用户可能不会注意到。
http://jsperf.com/function-call-overhead-test上的基准测试试图量化各种JS解释器中的函数调用开销。如果您担心,可以组合一个类似的基准测试来明确测试递归。
请注意,在EcmaScript 3中正确执行尾递归优化很困难。
例如,在JavaScript中实现简单的数组折叠:
function fold(f, x, i, arr) {
if (i === arr.length) { return x; }
var nextX = f(x, arr[i]);
return fold(f, nextX, i+1, arr);
}
由于调用,无法进行尾部优化
fold(eval, 'var fold=alert', 0, [0])
在 fold
的正文中执行 eval('var fold=alert')
会导致表面上的尾调用 fold
不实际进行递归。
EcmaScript 5 更改了 eval
的调用方式,使其除了通过名称 eval
调用外不可被调用,并且严格模式阻止 eval
引入局部变量,但是尾调用优化取决于静态确定尾调用位置的能力,在像 JavaScript 这样的动态语言中并不总是可能。
我对以下比较结果感到惊讶,你可以看到它们之间的差异。通常情况下,迭代更快,但是如果你需要执行大量递归操作,则差异可能会很大。
function factorialRec(n){
if (n > 0) {
return n * factorialRec(n - 1)
} else {
return 1;
}
}
function factorial(n){
var returnme = n;
for(var i = n - 1; i > 0; i--){
returnme *= i;
}
return returnme;
}
console.time('recursion')
for(var i=0;i<1000000;i++){
factorialRec(100);
}
console.timeEnd('recursion')
console.time('iteration')
for(var i=0;i<1000000;i++){
factorial(100);
}
console.timeEnd('iteration')
注意:虽然上面的示例非常有说服力,但实际上,您不应该遇到这么多的函数调用。因此,大多数情况下,减速不应与此问题有关。
var stack = new Array(size); var stack_index = 0;
- SapphireSun