簡體   English   中英

Pharo 是否提供尾調用優化?

[英]Does Pharo provide tail-call optimisation?

Pharo 中Integer>>#factorial的實現是:

factorial
        "Answer the factorial of the receiver."

        self = 0 ifTrue: [^ 1].
        self > 0 ifTrue: [^ self * (self - 1) factorial].
        self error: 'Not valid for negative integers'

這是一個尾遞歸定義。 但是,我可以在工作區中評估10000 factorial而不會出錯。

Pharo 是否在任何情況下執行尾調用優化,它是否在執行其他一些優化,或者只是使用非常深的堆棧?

Pharo 的執行模式並沒有什么神秘之處。 遞歸片段

^ self * (self - 1) factorial

這發生在第二個ifTrue:編譯為以下字節碼序列:

39 <70> self                  ; receiver of outer message *
40 <70> self                  ; receiver of inner message -
41 <76> pushConstant: 1       ; argument of self - 1
42 <B1> send: -               ; subtract
43 <D0> send: factorial       ; send factorial (nothing special here!) 
44 <B8> send: *               ; multiply
45 <7C> returnTop             ; return

請注意,在第 43 行沒有什么特別的事情發生。 如果選擇器是任何其他選擇器,代碼只是以與它相同的方式發送factorial 特別是我們可以看到這里沒有對堆棧進行特殊操作。

這並不意味着底層本機代碼不能進行優化。 但這是一個不同的討論。 對程序員來說重要的是執行模型,因為字節碼下的任何優化都旨在在概念級別支持該模型。

更新

有趣的是,非遞歸版本

factorial2
  | f |
  f := 1.
  2 to: self do: [:i | f := f * i].
  ^f

比遞歸(Pharo)慢一點。 原因一定是增加i的開銷比遞歸發送機制要大一點。

以下是我嘗試過的表達方式:

[25000 factorial] timeToRun
[25000 factorial2] timeToRun

這是一個非常深的堆棧。 或者更確切地說,根本沒有堆棧。

Pharo 是 Squeak 的后代,它直接從 Smalltalk-80 繼承了它的執行語義。 沒有線性固定大小的堆棧,而是每個方法調用都會創建一個新的MethodContext對象,該對象為每個遞歸調用中的參數和臨時變量提供空間。 它還指向發送上下文(用於稍后返回)創建上下文鏈接列表(它就像調試器中的堆棧一樣顯示)。 上下文對象就像任何其他對象一樣在堆上分配。 這意味着調用鏈可以非常深,因為可以使用所有可用內存。 您可以檢查thisContext以查看當前活動的方法上下文。

分配所有這些上下文對象是昂貴的。 為了速度,現代虛擬機(例如 Pharo 中使用的 Cog VM)確實在內部使用了一個堆棧,它由鏈接的頁面組成,因此它也可以是任意大的。 上下文對象僅在需要時創建(例如在調試時)並引用隱藏的堆棧幀,反之亦然。 這個幕后的機制相當復雜,但幸運的是對 Smalltalk 程序員隱藏了。

恕我直言,假定對factorial進行尾遞歸調用的初始代碼

factorial
        "Answer the factorial of the receiver."

        self = 0 ifTrue: [^ 1].
        self > 0 ifTrue: [^ self * (self - 1) factorial].
        self error: 'Not valid for negative integers'

實際上不是。 Leandro 的回復報告的字節碼證明:

39 <70> self                  ; receiver of outer message *
40 <70> self                  ; receiver of inner message -
41 <76> pushConstant: 1       ; argument of self - 1
42 <B1> send: -               ; subtract
43 <D0> send: factorial       ; send factorial (nothing special here!) 
44 <B8> send: *               ; multiply
45 <7C> returnTop             ; return

returnTop之前有一個發送*而不是factorial 我會使用累加器寫一條消息作為

factorial: acc
    ^ self = 0
        ifTrue: [ acc ]
        ifFalse: [ self - 1 factorial: acc * self ]

產生這張圖片中報告的字節碼。

順便說一句,

n := 10000.
[n slowFactorial] timeToRun .
[n factorial] timeToRun.
[n factorial: 1] timeToRun.

第一個和第二個都需要 29 毫秒,最后一個在新的 Pharo 9 圖像上需要 595 毫秒。 為什么這么慢?

不,Pharo 及其 VM 不會優化遞歸尾調用。

從在 Pharo 9 圖像上運行測試可以明顯看出, 這篇關於該主題的碩士論文證實了這一點。

截至今天,Pharo 提供了兩種階乘方法,一種( Integer >> factorial )使用 2-partition 算法並且是最有效的,另一種看起來像這樣:

Integer >> slowFactorial [
    self > 0
        ifTrue: [ ^ self * (self - 1) factorial ].
    self = 0
        ifTrue: [ ^ 1 ].
    self error: 'Not valid for negative integers'
]

它具有外遞歸結構,但實際上仍然調用非遞歸階乘方法。 這可能解釋了為什么 Massimo Nocentini 在計時時得到幾乎相同的結果。

如果我們嘗試這個修改后的版本:

Integer >> recursiveFactorial [
    self > 0
        ifTrue: [ ^ self * (self - 1) recursiveFactorial ].
    self = 0
        ifTrue: [ ^ 1 ].
    self error: 'Not valid for negative integers'
]

我們現在有一個真正的遞歸方法,但是,正如 Massimo 指出的,它仍然不是遞歸。

這是尾遞歸:

tailRecursiveFactorial: acc
^ self = 0
    ifTrue: [ acc ]
    ifFalse: [ self - 1 tailRecursiveFactorial: acc * self ]

沒有尾調用優化,這個版本表現出迄今為止最差的性能,即使與recursiveFactorial相比也是如此。 我認為這是因為它給堆棧帶來了所有冗余的中間結果。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM