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