关于“尾部调用优化”的问题文章

发布于 2024-10-12 21:50:11 字数 693 浏览 3 评论 0原文

我对这篇文章有疑问。

在这段代码

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 呢?我也还不清楚。

I have a question regarding this article.

Between this code

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

and this code

(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) I'm confused about how much this helps. Does the second snippet simply have a tail call that creates less overhead because it calculates what it needs before it calls itself again, or is there something more I'm missing?

As I understand it, the tail call still isn't eliminated, just optimized.

2) And why does there need to be odds and odds1 anyway? It still isn't clear to me either.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

昨迟人 2024-10-19 21:50:11

我很困惑这有多大帮助。第二个片段是否只是有一个尾部调用,可以减少开销,因为它会在再次调用自身之前计算所需的内容,还是我还缺少更多内容?

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

如果过程的结尾如下所示:

push args
call foo
return

那么编译器可以对其进行优化,以

jump startOfFoo

完全消除过程调用。

为什么还需要有赔率和赔率1?我也还不清楚。

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

我认为,您可以将 odds1 称为 oddsImpl 之类的名称,这样会更清楚。

I'm confused about how much this helps. Does the second snippet simply have a tail call that creates less overhead because it calculates what it needs before it calls itself again, or is there something more I'm missing?

As I understand it, the tail call still isn't eliminated, just optimized.

If the end of a procedure looks like this:

push args
call foo
return

Then the compiler can optimize that away to just

jump startOfFoo

Eliminating the procedure call entirely.

And why does there need to be odds and odds1 anyway? It still isn't clear to me either.

The "contract" of odds specifies only two arguments - the third argument is just an implementation detail. So you hide that away in an internal method, and present a "wrapper" as the external API.

You could call odds1 something like oddsImpl and it would make it clearer, I think.

毁梦 2024-10-19 21:50:11

第一个版本不是尾递归,因为在获得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

odds 和odds1 只是在其他代码调用此函数时提供初始累加器值。

The first version isn't tail recursive because after getting the value of odds(n - 1, p - 1) it has to then multiply it by (n / p), the second version moves this into the calculation of the parameters for the odds1 function to make it properly tail recursive.

If you look at the call stack then the first would go like this:

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

whereas the second would be:

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

because you're simply returning the value of the recursive call the compiler can optimize this easily:

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

The reason for having odds and odds1 is simply to supply the initial accumulator value when other code calls this function.

梦萦几度 2024-10-19 21:50:11

尾递归的优化如下,在第一个示例中,因为您无法计算乘法的结果 return (n / p) * odds(n - 1, p - 1) 直到您称为 odds(n-1),解释器必须保存我们在内存中(在堆栈上)的当前位置,并打开对 odds 的新调用。

递归地,这也会在下一次调用以及之后的调用中发生,依此类推。因此,当我们到达递归末尾并开始返回值并计算乘积时,我们有 n 个待处理操作。

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

把它想象成书籍参考文献。想象一下,您打开一本食谱,找到某个食谱,配料列出如下:

  1. 加盐
  2. 下一页的配料

下一页的配料加

  1. 胡椒粉
  2. 下一页的配料

等等。您如何知道所有配料是什么?你必须记住你在每一页上看到的内容!

尽管第二个示例更像是以下成分列表:

  1. 忘记这个,只需使用您在下一页上看到的内容下一页

有:

  1. 胡椒
  2. 忘记这个,只需使用您在下一页上看到的内容

等。到达最后一页(请注意,这个类比是准确的,因为两者都采用相同数量的函数调用),您拥有所有成分,而不必“保留在内存中”您在每一页上看到的内容,因为它们都在最后一页!

The optimisation of tail recursion is as follows, in the first example since you cannot calculate the result of the multiplication return (n / p) * odds(n - 1, p - 1) until you've called odds(n-1), the interperator has to hold our current position in memory (on the stack), and open a new call to odds.

Recursively, that will happen in the next call as well, and the one after it and so forth. So we have n pending operations by the time we reach the end of our recursion and start returning the valuesand calculating the products.

In the second example, since the return statement executed is simply return odds1(n - 1, p - 1, (n / p) * acc) we can calculate the function arguments, and simply call the odds1(n-1) without holding our current position. this is where the optimisation is, because now i dont have to remember where i am every time i open a new frame on the stack.

Think of it like book references. imagine you open a cookbook and go to a certain recipe, and the ingredients are listed as follows:

  1. salt
  2. the ingredients on the next page

the next page has

  1. pepper
  2. the ingredients on the next page

etc. how do you tell what all the ingredients are? you have to remember what you saw on every page!

although the second example is more like the following ingredients list:

  1. salt
  2. forget this, just use what you see on the next page

the next page has:

  1. salt
  2. pepper
  3. forget this, just use what you see on the next page

etc. by the time you reach the last page (note that the analogy is accurate in that both take the same amount of function calls), you have all the ingredients, without having to "keep in memory" what you saw on every page, because it's all there on the last page!

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文