性能:JavaScript中的递归与迭代


24

我最近阅读了一些有关Javascript的功能方面以及Scheme与Javascript之间关系的文章(例如http://dailyjs.com/2012/09/14/functional-programming/)(后者受前者的影响,是一种功能性语言,而OO方面则继承自Self(一种基于原型的语言)。

但是我的问题更具体:我想知道是否有关于Java递归与迭代性能的指标。

我知道在某些语言中(通过设计迭代执行得更好),差异很小,因为解释器/编译器将递归转换为迭代,但是我猜可能不是Javascript的情况,因为它至少部分是功能语言。


3
进行自己的测试,然后立即在jsperf.com上
TehShrike

悬赏金和一个回答提到了TCO。看来ES6指定了TCO,但如果我们相信kangax.github.io/compat-table/es6,到目前为止还没有人实施它,我是否缺少某些东西?
Matthias Kauer

Answers:


28

JavaScript 不会执行尾部递归优化,因此,如果递归太深,则可能会导致调用堆栈溢出。迭代没有这样的问题。如果您认为您将要进行过多的递归,并且确实需要递归(例如,进行洪水填充),请使用您自己的堆栈替换递归。

递归性能可能比迭代性能更差,因为函数调用和返回需要状态保存和恢复,而迭代只是跳到函数中的另一点。


只是想知道...我已经看到了一些代码,其中创建了一个空数组,并将递归函数站点分配给数组中的某个位置,然后返回存储在数组中的值。这是“用您自己的堆栈替换递归”的意思吗?例如: var stack = []; var factorial = function(n) { if(n === 0) { return 1 } else { stack[n-1] = n * factorial(n - 1); return stack[n-1]; } }
mastazi

@mastazi:不,这将使内部的堆栈无效。我的意思是类似于Wikipedia中基于队列的洪水填充
Triang3l

值得注意的是,一种语言不执行TCO,但可以执行。人们优化JS的方式意味着TCO可能会出现在一些实现中
Daniel Gratzer

1
@mastazi替换else在与该功能else if (stack[n-1]) { return stack[n-1]; } else,你有记忆化。编写该析因代码的人执行的代码不完整(可能应该在stack[n]各处使用而不是stack[n-1])。
2013年

谢谢@Izkata,我经常进行这种优化,但是直到今天我还不知道它的名字。我本该研究CS而不是IT ;-)
mastazi

20

更新:自ES2015起,JavaScript具有TCO,因此下面的部分论点不再成立。


尽管Javascript没有尾调用优化,但是递归通常是最好的方法。诚挚的,除了在极端情况下,您不会获得调用堆栈溢出。

性能是要牢记的,但过早优化也是。如果您认为递归比迭代更优雅,那就去做吧。如果事实证明这是您的瓶颈(可能永远不会),那么您可以用一些难看的迭代代替它。但大多数时候,瓶颈在于DOM操作或更普遍的I / O,而不是代码本身。

递归总是更优雅1

1:个人意见。


3
我同意递归更加优雅,并且优雅很重要,因为它既具有可读性又具有可维护性(这是主观的,但我认为递归非常易于阅读,因此易于维护)。但是,有时性能很重要。您是否可以支持递归是最好的方法(甚至在性能方面)?
mastazi

3
正如我在回答中所说的,@ mastazi,我怀疑递归是否会成为您的瓶颈。在大多数情况下,这是DOM操作,或更普遍的是I / O。而且不要忘了,过早的优化是万恶之源;)
Florian Margaine 2012年

多数情况下+1是DOM操作的瓶颈!我记得关于Yehuda Katz(Ember.js)的一次非常有趣的采访。
mastazi

1
@mike “过早”定义是“在适当的时间之前成熟或成熟”。如果您知道递归地执行某些操作会导致堆栈溢出,那么这还为时过早。但是,如果您一时兴起(没有任何实际数据),那还为时过早。
Zirak

2
使用Javascript,您不会获得程序有多少堆栈。您可能在IE6中有一个很小的堆栈,在FireFox中有一个很大的堆栈。递归算法很少具有固定深度,除非您执行Scheme风格的递归循环。似乎非基于循环的递归似乎无法避免过早的优化。
mike30

7

我也对javascript的这种性能很好奇,所以我做了一些实验(尽管在旧版本的node上)。我相对于迭代递归编写了阶乘计算器,并在本地运行了几次。结果似乎严重偏向于要征税的递归(预期)。

代码:https//github.com/j03m/trickyQuestions/blob/master/factorial.js

Result:
j03m-MacBook-Air:trickyQuestions j03m$ node factorial.js 
Time:557
Time:126
j03m-MacBook-Air:trickyQuestions j03m$ node factorial.js 
Time:519
Time:120
j03m-MacBook-Air:trickyQuestions j03m$ node factorial.js 
Time:541
Time:123
j03m-MacBook-Air:trickyQuestions j03m$ node --version
v0.8.22

您可以尝试一下"use strict";,看看是否有所作为。(它将产生jumps而不是标准呼叫顺序)
Burdock 2015年

1
在最新版本的node(6.9.1)上,我得到了极为相似的结果。递归有一些负担,但我认为这是一个极端情况-1,000,000个循环的400ms差异是每个循环0.0025 ms。如果要执行1,000,000次循环,则要牢记。
Kelz '17

6

根据OP的要求,我会插手(不要装傻,希望:P)

我认为我们都同意递归只是一种更优雅的编码方式。如果做得好,它可以使代码更易于维护,恕我直言,减少0.0001ms同样重要(如果不是更多的话)。

至于关于JS不执行尾部调用优化的说法,这已经不完全正确了,使用ECMA5的严格模式可以启用TCO。不久前我对此不太满意,但是至少我现在知道为什么arguments.callee在严格模式下抛出错误。我知道上面的链接链接到一个错误报告,但是该错误设置为WONTFIX。此外,即将发布标准的TCO:ECMA6(2013年12月)。

本能地坚持JS的功能本质,我想说递归是99.99%的时间更有效的编码风格。但是,弗洛里安·玛格恩(Florian Margaine)指出,瓶颈可能在其他地方发现。如果要操作DOM,则最好将精力集中在编写尽可能可维护的代码上。DOM API是什么:慢。

我认为几乎不可能就哪个是更快的选项提供确切的答案。最近,我看到的许多jspref都表明Chrome的V8引擎在某些任务上可笑地快,在FF的SpiderMonkey上运行速度慢了4倍,反之亦然。现代JS引擎可以进行各种技巧来优化您的代码。我不是专家,但是我确实认为例如V8已针对闭包(和递归)进行了高度优化,而MS的JScript引擎却没有。当涉及DOM时,SpiderMonkey的性能通常会更好。

简而言之:我想说哪种技术会更出色,就像在JS中一样,几乎无法预测。


3

如果没有严格模式,则迭代性能通常递归快(除了使JIT做更多工作之外)。尾递归优化从根本上消除了任何明显的差异,因为它将整个调用序列转换为跳转。

示例:Jsperf

在递归和迭代之间进行选择时,我建议更多地担心代码的清晰度和简单性。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.