LISP或ML如何实现尾调用优化?
答案 0 :(得分:6)
我不能谈论不同的编译器/解释器的确切实现细节,但一般来说尾调用优化的操作如下:
通常函数调用涉及这样的事情:
但是当一个函数处于尾部位置时,这意味着你要返回你要调用的函数的结果,你可能会很棘手并且
请注意,#1和#2实际上并不涉及任何工作,#3可能很棘手或简单,具体取决于您的实现,4-7不涉及您正在调用的函数中的任何特殊内容。还要注意,所有这些都会导致相对于调用堆栈的0堆栈增长,因此这允许infinte递归并且通常可以加快速度。
另请注意,这种优化不仅可以应用于直接递归函数,还可以应用于共同递归函数,实际上所有调用都处于尾部位置。
答案 1 :(得分:3)
它的CPU架构和/或操作系统依赖于哪种功能可以进行尾部调用优化。这是因为调用约定(用于传递函数参数和/或在函数之间传递控制)在CPU和/或操作系统之间是不同的。它通常归结为尾部调用中传递的任何内容是否必须来自堆栈。例如,采用如下函数:
void do_a_tailcall(char *message)
{
printf("Doing a tailcall here; you said: %s\n", message);
}
如果你编译它,即使是高优化(-O8 -fomit-frame-pointer
),在32位x86(Linux)上,你得到:
do_a_tailcall:
subl $12, %esp
movl 16(%esp), %eax
movl $.LC0, (%esp)
movl %eax, 4(%esp)
call printf
addl $12, %esp
ret
.LC0:
.string "Doing a tailcall here; you said: %s\n"
,即。一个经典函数,具有堆栈框设置/拆卸(subl $12, %esp
/ addl $12, %esp
)和函数中的显式ret
。
在64位x86(Linux)中,这看起来像:
do_a_tailcall:
movq %rdi, %rsi
xorl %eax, %eax
movl $.LC0, %edi
jmp printf
.LC0:
.string "Doing a tailcall here; you said: %s\n"
所以它得到尾部优化。
在完全不同类型的CPU架构(SPARC)上,这看起来像(我已将编译器的注释留在其中):
.L16:
.ascii "Doing a tailcall here; you said: %s\n\000"
!
! SUBROUTINE do_a_tailcall
!
.global do_a_tailcall
do_a_tailcall:
sethi %hi(.L16),%o5
or %g0,%o0,%o1
add %o5,%lo(.L16),%o0
or %g0,%o7,%g1
call printf ! params = %o0 %o1 ! Result = ! (tail call)
or %g0,%g1,%o7
又一个...... ARM(Linux EABI):.LC0:
.ascii "Doing a tailcall here; you said: %s\012\000"
do_a_tailcall:
@ args = 0, pretend = 0, frame = 0
@ frame_needed = 0, uses_anonymous_args = 0
@ link register save eliminated.
mov r1, r0
movw r0, #:lower16:.LC0
movt r0, #:upper16:.LC0
b printf
这里的差异是参数传递的方式,并且控制权被转移:
32位x86(stdcall
/ cdecl
类型调用)在堆栈上传递args,因此尾调用优化的可能性非常有限 - 除了特定的角柜,它只有可能恰好参数passthrough或尾调用函数完全没有参数。
64位x86(UNIX x86_64
样式,但在Win64上没有太大的不同)在寄存器中传递了一定数量的参数,这使编译器在可以调用的内容上更加自由,而无需通过堆栈上的任何东西。通过jmp
进行控制转移只会使尾部调用函数继承堆栈 - 包括最顶层的值,它将是我们do_a_tailcall
的原始调用者的返回地址。 / p>
SPARC不仅在寄存器中传递函数参数,还在返回地址(它使用链接寄存器,{{1 }})。因此,当您通过%o7
传输控制时,实际上并不强制新的堆栈帧,因为它所做的只是设置链接寄存器和程序计数器...以通过另一个来撤消前者SPARC的奇怪特性,即所谓的延迟槽指令(call
- or %g0,%g1,%o7
的sparc-ish - 在 {{}之后执行 1}}但之前达到了mov %g1,%o7
的目标。上面的代码是从旧的编译器rev ...创建的,而不是理论上可以优化的......
ARM类似于SPARC,因为它使用链接寄存器,尾递归函数只是将未修改/未触发的函数传递给尾调用。它也类似于x86,在尾递归上使用call
(分支)而不是“call”等价(call
,branch-and-link)。
在所有至少可以在寄存器中传递参数的架构中,编译器可以在各种函数上应用尾调用优化。