[英]Question on “Tail Call Optimization” Article
我对这篇文章有疑问。
这段代码之间
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
呢? 我还是不清楚。
我对这有多大帮助感到困惑。 第二个代码段是否只是简单地进行了尾部调用,因为它可以在再次调用自身之前计算出所需的内容,从而减少了开销,还是我还缺少更多的功能?
据我了解,尾调用仍然没有消除,只是优化了。
如果过程结束看起来像这样:
push args
call foo
return
然后编译器可以将其优化为
jump startOfFoo
完全消除过程调用。
为什么仍然需要几率? 我还是不清楚。
odds
的“合同”仅指定两个参数-第三个参数仅是实现细节。 因此,您可以将其隐藏在内部方法中,并提供“包装器”作为外部API。
你可以称之为odds1
像oddsImpl
,它会更清楚,我想。
第一个版本不是尾部递归,因为在获得odds(n - 1, p - 1)
的值之后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
的原因odds1
是在其他代码调用此函数时提供初始累加器值。
尾递归的优化如下,在第一个示例中,因为直到调用odds(n-1),您才能计算乘法return (n / p) * odds(n - 1, p - 1)
的结果return (n / p) * odds(n - 1, p - 1)
-1) ,操作者必须将我们当前的位置保存在内存中(在堆栈上),并打开一个新的赔率电话。
递归地,这也将在下一个调用中以及随后的调用中发生,依此类推。 因此,到递归结束并开始返回值并计算乘积时,我们将有n个待处理操作。
在第二个示例中,由于执行的return语句只是return odds1(n - 1, p - 1, (n / p) * acc)
我们可以计算函数参数,而无需保持就可以简单地调用odds1(n-1) 我们目前的职位 。 这就是优化所在,因为现在我不必每次在堆栈上打开新框架时都记得自己在哪里。
可以将其视为书籍参考。 想象您打开一本食谱并转到某个食谱,其食材如下所示:
下一页有
等等。你怎么知道所有成分是什么? 您必须记住在每一页上看到的内容!
尽管第二个示例更像以下成分列表:
下一页有:
等等。等到到达最后一页时(注意,类推是准确的,因为两者都调用相同数量的函数),您便拥有了所有要素,而不必“保留在内存中”在每一页上看到的内容,因为所有内容都在最后一页!
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.