为什么递归比JavaScript上的求和函数的for循环更快?

时间:2013-10-05 22:18:23

标签: javascript optimization compiler-construction

我正在使用一种可转换为JavaScript的语言。为了避免一些堆栈溢出,我通过将某些函数转换为for循环来应用尾调用优化。令人惊讶的是,转换比递归版本更快。

http://jsperf.com/sldjf-lajf-lkajf-lkfadsj-f/5

递归版:

(function recur(a0,s0){
    return a0==0 ? s0 : recur(a0-1, a0+s0)
})(10000,0)

尾调用优化后:

ret3 = void 0;
a1   = 10000;
s2   = 0;
(function(){
    while (!ret3) {
        a1 == 0 
            ? ret3     = s2
            : (a1_tmp$ = a1 - 1 ,
               s2_tmp$ = a1 + s2,
               a1      = a1_tmp$,
               s2      = s2_tmp$);
     }
})();
ret3;

使用Google Closure Compiler进行一些清理后:

ret3 = 0;
a1   = 1E4;
for(s2 = 0; ret3 == 0;)
    0 == a1 
        ? ret3     = s2 
        : (a1_tmp$ = a1 - 1 ,
           s2_tmp$ = a1 + s2,
           a1      = a1_tmp$,
           s2      = s2_tmp$);
c=ret3;

递归版本比“优化”版本更快!如果递归版本必须处理数以千计的上下文更改,那怎么可能呢?

3 个答案:

答案 0 :(得分:5)

优化比尾部调用优化还要多。

例如,我注意到你正在使用两个临时变量,只需要:

s2 += a1;
a1--;

仅这一点实际上将操作次数减少了三分之一,导致性能提高了50%

从长远来看,在尝试优化操作之前优化正在执行的操作非常重要。

编辑:这是更新后的jsperf

答案 1 :(得分:2)

as Kolink 说出你的代码片段只是将n添加到总数中,将n减少1,然后循环直到n无法覆盖0

所以就这样做:

n = 10000, o = 0; while(n) o += n--;

它是 more faster并且比递归版本可见,并且当然输出same result

答案 2 :(得分:2)

递归版本中没有太多的上下文更改,因为命名函数recur包含在recur本身的范围内/它们共享相同的范围。其原因与JavaScript引擎评估范围的方式有关,并且有很多网站可以解释这个主题,所以我不会在这里做。再看一下,你会注意到recur也是一个所谓的“纯”函数,这基本上意味着只要内部执行运行它就永远不必离开它自己的范围(简单地说:直到它返回一个值) )。这两个事实使它基本上很快。我只想在这里提一下,第一个例子是唯一的尾部调用优化了三个中的一个 - tc优化只能在递归函数中完成,这是唯一的递归函数。

然而,第二个看第二个例子(没有双关语)揭示了“优化器”让你变得更糟,因为它通过将操作分成

将范围引入前一个纯函数
  • 变量而不是参数
  • 一个while循环
  • 一个IIFE(立即调用的函数表达式),用于分隔引入的内部和外部变量

这导致性能较差,因为现在引擎必须处理10000个上下文更改。

说实话我不知道为什么第三个例子的性能比递归的差,所以可能与它有关:

  • 您使用的浏览器(曾经尝试过另一个并比较结果?)
  • 变量数
  • 由for循环创建的堆栈帧(从未听说过),其中 与第一个例子有关:JS引擎解释了一个 纯递归函数,直到找到一个return语句。如果语句后面的最后一件事是函数调用,那么计算任何表达式(如果有的话)和变量作为参数传递,调用函数并丢弃框架
  • 东西,只有浏览器供应商才能真正告诉你:)