关于“尾部调用优化”的问题文章
我对这篇文章有疑问。
在这段代码
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) 为什么还需要有 odds
和 odds1
呢?我也还不清楚。
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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(3)
如果过程的结尾如下所示:
那么编译器可以对其进行优化,以
完全消除过程调用。
odds
的“契约”仅指定两个参数 - 第三个参数只是一个实现细节。因此,您将其隐藏在内部方法中,并提供一个“包装器”作为外部 API。我认为,您可以将
odds1
称为oddsImpl
之类的名称,这样会更清楚。If the end of a procedure looks like this:
Then the compiler can optimize that away to just
Eliminating the procedure call entirely.
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 likeoddsImpl
and it would make it clearer, I think.第一个版本不是尾递归,因为在获得
odds(n - 1, p - 1)
然后必须将其乘以(n / p)
,第二个版本将其移至odds1
参数的计算中> 函数使其正确尾递归。如果您查看调用堆栈,那么第一个将是这样的:
而第二个将是:
因为您只是返回递归调用的值,所以编译器可以轻松优化它:
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 theodds1
function to make it properly tail recursive.If you look at the call stack then the first would go like this:
whereas the second would be:
because you're simply returning the value of the recursive call the compiler can optimize this easily:
The reason for having
odds
andodds1
is simply to supply the initial accumulator value when other code calls this function.尾递归的优化如下,在第一个示例中,因为您无法计算乘法的结果
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) 不持有当前职位。这就是优化的地方,因为现在我不必每次在堆栈上打开新框架时都记住我在哪里。把它想象成书籍参考文献。想象一下,您打开一本食谱,找到某个食谱,配料列出如下:
下一页的配料加
等等。您如何知道所有配料是什么?你必须记住你在每一页上看到的内容!
尽管第二个示例更像是以下成分列表:
有:
等。到达最后一页(请注意,这个类比是准确的,因为两者都采用相同数量的函数调用),您拥有所有成分,而不必“保留在内存中”您在每一页上看到的内容,因为它们都在最后一页!
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:
the next page has
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:
the next page has:
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!