Pharo是否提供尾调优化?

时间:2016-05-07 14:33:36

标签: smalltalk tail-recursion pharo

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是否可以在任何情况下执行尾调用优化,是进行其他优化,还是使用非常深的堆栈?

4 个答案:

答案 0 :(得分:7)

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

答案 1 :(得分:7)

这是一个非常深的堆栈。或者更确切地说,根本没有堆叠。

Pharo是Squeak的后代,它直接从Smalltalk-80继承了它的执行语义。没有线性固定大小的堆栈,相反,每个方法调用都会创建一个新的MethodContext对象,该对象在每次递归调用中为参数和临时变量提供空间。它还指向发送上下文(以便稍后返回)创建上下文的链接列表(它在调试器中显示为堆栈)。上下文对象在堆上分配,就像任何其他对象一样。这意味着调用链可能非常深,因为可以使用所有可用内存。您可以检查thisContext以查看当前活动的方法上下文。

分配所有这些上下文对象非常昂贵。对于速度,现代VM(例如Pharo中使用的Cog VM)实际上在内部使用堆栈,其由链接页面组成,因此它也可以任意大。上下文对象仅在需要时创建(例如在调试时)并且引用隐藏的堆栈帧,反之亦然。幕后的这种机制非常复杂,但幸运的是,Smalltalk程序员隐藏了这一点。

答案 2 :(得分:0)

恕我直言,假设初始代码具有对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's reply报告的字节码证明:

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 ]

产生this picture中报告的字节码。

顺便说一句,

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

第一个和第二个都需要29毫秒,最后一个要595毫秒。为什么这么慢?

答案 3 :(得分:0)

不,Pharo 及其 VM 不会优化递归尾调用。

在 Pharo 9 图像上运行测试很明显,这 master thesis on the subject 证实了这一点。

截至今天,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'
]

它有一个外递归结构,但实际上仍然调用非递归的factorial方法。这可能解释了为什么 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 相比也是如此。我认为这是因为它给堆栈带来了所有冗余的中间结果。