迭代比递归更快,还是更不容易出现堆栈溢出?

时间:2012-02-28 00:13:00

标签: javascript recursion iteration stack-overflow

我知道你可以使用一个简单的循环来重写一个递归函数,使用一个数组作为'work to be done'的先进先出队列。我听说这使得堆栈溢出的可能性降低。

但是如果堆栈溢出不是问题(因为你没有非常深地递归),有没有理由更喜欢迭代而不是递归?它更快吗?

我最感兴趣的是V8上的JavaScript。

3 个答案:

答案 0 :(得分:20)

在Javascript中,没有(不需要,也许不能?看到注释)做尾递归优化,递归比迭代慢(因为,几乎在所有语言中,函数调用很多比跳转更昂贵)并且如果你递得太深,可能会导致堆栈溢出错误(但是,限制可能非常深; Chrome在我的实验中放弃了递归深度16,316)

但是,性能影响有时值得你编写递归函数时得到的代码清晰度,而某些事情在没有递归的情况下更难做(递归函数几乎总是很多比他们的迭代对应物短),例如使用树(但是你并没有真正用Javascript做到这一点编辑: GGG提到DOM是一棵树,并且在JS中使用它很常见。)

答案 1 :(得分:4)

递归可能会更快失败,因为无限递归函数会炸掉堆栈,从而产生程序可以恢复的异常,而迭代解决方案将一直运行直到外部代理停止。

对于在给定时间内产生有效输出的代码,递归的主要成本是函数调用开销。迭代解决方案根本没有这个,所以倾向于在没有明确优化递归的语言中赢得性能关键代码。

在基准测试中肯定会引人注目,但除非您编写性能关键代码,否则您的用户可能不会注意到。

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])

eval('var fold=alert')的主体内fold,导致对fold的看似尾递归调用实际上不是递归的。

EcmaScript 5将eval更改为不可调用,除非通过名称eval,并且严格模式阻止eval引入局部变量,但尾调优化取决于静态的能力确定尾调用的位置,这在JavaScript等动态语言中并不总是可行。

答案 2 :(得分:1)

这取决于......当我在IE8中遇到递归函数中的“慢速脚本”错误时,我问了同样的问题。并且很惊讶迭代实际上甚至更慢。

我正在寻找一个特定节点的树。我已经使用类似的方法将我的递归函数重写为迭代方式(使用堆栈来保持上下文):https://stackoverflow.com/a/159777/1245231

之后我开始从IE 8获得比以前更多的“慢脚本”错误。进行一些分析确认迭代版本更慢。

原因可能是在JS中使用方法调用堆栈可能比在循环中使用具有相应push()和pop()操作的数组更快。为了验证这一点,我创建了在两种情况下模拟树木行走的测试:http://jsperf.com/recursion-vs-iteration-852结果令人惊讶。在Chrome中,迭代版本(在我的情况下)慢了19%,在IE8中,迭代版本比递归版本慢65%。