Smalltalk Pharo是否提供尾部呼叫优化?

Smalltalk Pharo是否提供尾部呼叫优化?,smalltalk,tail-recursion,pharo,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' 这是一个尾部递归定义。但

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阶乘


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
比递归的(Pharo)要慢一点。原因一定是与增加
i
相关的开销比递归发送机制稍微大一点

以下是我尝试过的表达方式:

[25000 factorial] timeToRun
[25000 factorial2] timeToRun

这真是一堆。或者更确切地说,根本没有堆栈

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


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

IMHO,初始代码被认为具有对
阶乘的尾部递归调用

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'
实际上不是。通过以下证明报告的字节码:

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
它生成中报告的字节码

顺便说一句


第一个和第二个都需要29毫秒,最后一个在新的Pharo 9图像上需要595毫秒。为什么这么慢?

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

通过对Pharo9图像进行测试可以明显看出,这证实了这一点

到目前为止,Pharo提供了两种阶乘方法,一种(整数>>阶乘)使用2分割算法,效率最高,另一种如下所示:

Integer >> slowFactorial [
    self > 0
        ifTrue: [ ^ self * (self - 1) factorial ].
    self = 0
        ifTrue: [ ^ 1 ].
    self error: 'Not valid for negative integers'
]
它有一个外部递归结构,但实际上仍然调用非递归的阶乘方法。这可能解释了为什么马西莫·诺森蒂尼在计时时得到了几乎相同的结果

如果我们尝试此修改版本:

Integer >> recursiveFactorial [
    self > 0
        ifTrue: [ ^ self * (self - 1) recursiveFactorial ].
    self = 0
        ifTrue: [ ^ 1 ].
    self error: 'Not valid for negative integers'
]
我们现在有了一个真正的递归方法,但正如Massimo指出的,它仍然不是tail递归的

这是尾部递归:

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

在没有尾部调用优化的情况下,该版本显示出迄今为止最差的性能,即使与递归阶乘相比也是如此。我认为这是因为它给堆栈增加了所有冗余中间结果的负担。

您可以在第一个
ifTrue:
案例中放置一个断点,然后计算相同方法在堆栈上的次数…;-)好的,这只是普通的递归调用。那么,为什么法罗没有溢出堆栈?你能给我介绍一下描述执行模型的东西吗?@WilfredHughes有些情况下,你不能使用递归,因为你可能会耗尽堆栈。在阶乘的情况下,情况通常不是这样,因为每个递归都只是一个没有参数的调用,因此只有接收方和返回地址被推送到本机堆栈上。同时,阶乘增长如此之快,以至于通常在消耗所有堆栈空间之前很久就达到了所需的结果。对于执行模型pelase,请查看Smalltalk-80语言及其实现。它是在线的。上面提到的这本书的url是[link](),很难相信这本书是30年前写的。那么,法罗的执行模式接近R6RS方案?但有点相反:Scheme构造了continuations“forward”——下一步执行什么,而Pharo有一个指向上一个上下文的指针——一旦完成就返回到那里。还是我遗漏了什么?
Integer >> slowFactorial [
    self > 0
        ifTrue: [ ^ self * (self - 1) factorial ].
    self = 0
        ifTrue: [ ^ 1 ].
    self error: 'Not valid for negative integers'
]
Integer >> recursiveFactorial [
    self > 0
        ifTrue: [ ^ self * (self - 1) recursiveFactorial ].
    self = 0
        ifTrue: [ ^ 1 ].
    self error: 'Not valid for negative integers'
]
tailRecursiveFactorial: acc
^ self = 0
    ifTrue: [ acc ]
    ifFalse: [ self - 1 tailRecursiveFactorial: acc * self ]