关于“尾调优化”的问题

时间:2011-01-16 20:38:33

标签: javascript optimization tail-recursion article

我对this文章有疑问。

在此代码之间

function odds(n, p) {
  if(n == 0) {
    return 1
  } else {
    return (n / p) * odds(n - 1, p - 1)
  }
}

和此代码

(function(){
  var odds1 = function(n, p, acc) {
    if(n == 0) {
      return acc
    } else {
      return odds1(n - 1, p - 1, (n / p) * acc)
    }
  }

  odds = function(n, p) {
    return odds1(n, p, 1)
  }  
})()

1)我对这有多大帮助感到困惑。第二个片段是否只是一个尾部调用,它会产生更少的开销,因为它会在它再次调用之前计算它需要的内容,或者是否还有一些我缺少的内容?

据我所知,尾调用仍然没有消除,只是优化了。

2)为什么还需要oddsodds1?我现在还不清楚。

3 个答案:

答案 0 :(得分:1)

  

我对这有多大帮助感到困惑。第二个片段是否只是一个尾部调用,它会产生更少的开销,因为它会在它再次调用之前计算它需要的内容,或者是否还有一些我缺少的内容?

     

据我所知,尾调用仍然没有消除,只是优化了。

如果程序结束如下:

push args
call foo
return

然后编译器可以将其优化为

jump startOfFoo

完全取消程序调用。

  

为什么还需要赔率和赔率?我现在还不清楚。

odds的“契约”只指定了两个参数 - 第三个参数只是一个实现细节。因此,您将其隐藏在内部方法中,并将“包装器”作为外部API。

我可以将odds1称为oddsImpl,我认为这会更清晰。

答案 1 :(得分:1)

第一个版本不是tail recursive,因为在获得odds(n - 1, p - 1)的值后,必须将其乘以(n / p),第二个版本会将其移动到参数的计算中odds1函数使尾部递归正确。

如果你看一下调用堆栈,那么第一个就是这样:

odds(2, 3)
  odds(1, 2)
    odds(0, 1)
    return 1
  return 1/2 * 1
return 2/3 * 1/2

而第二个是:

odds(2, 3)
  odds1(2, 3, 1)
    odds1(1, 2, 2/3)
      odds1(0, 1, 1/2 * 2/3)
      return 1/3
    return 1/3
  return 1/3
return 1/3

因为您只是返回递归调用的值,编译器可以轻松地优化它:

odds(2, 3)
#discard stackframe
odds1(2, 3, 1)
#discard stackframe
odds1(1, 2, 2/3)
#discard stackframe
odds1(0, 1, 1/3)
return 1/3

oddsodds1的原因只是在其他代码调用此函数时提供初始累加器值。

答案 2 :(得分:1)

尾递归的优化如下,在第一个例子中,因为在你调用赔率(n-1)之前你不能计算乘法return (n / p) * odds(n - 1, p - 1)的结果,所以交织者必须保持我们当前的位置在内存中(在堆栈上),并打开一个新的赔率调用。

递归地,这将在下一个调用中发生,并且在它之后发生,依此类推。因此,当我们到达递归结束并开始返回值并计算产品时,我们有n个待处理的操作。

在第二个例子中,由于执行的return语句只是return odds1(n - 1, p - 1, (n / p) * acc),我们可以计算函数参数,只需调用odds1(n-1)而不保持当前位置。这是优化的地方,因为现在我不必记住我每次在堆栈上打开一个新框架时的位置。

把它想象成书籍参考。想象一下你打开一本食谱然后去一个食谱,其成分列举如下:

  1. 下一页的成分
  2. 下一页有

    1. 胡椒
    2. 下一页的成分
    3. 等。你怎么知道所有的成分是什么?你必须记住你在每一页上看到的内容!

      虽然第二个例子更像是以下成分列表:

      1. 忘记这一点,只需使用你在下一页看到的内容
      2. 下一页有:

        1. 胡椒
        2. 忘记这一点,只需使用你在下一页看到的内容
        3. 等。当你到达最后一页时(注意类比是准确的,因为两者都需要相同数量的函数调用),你拥有所有的成分,而不必“记住”你在每一页上看到的内容,因为它是最后一页都在那里!