[英]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.