如何在汇编级实现FPL中的尾调用优化?

时间:2012-10-22 18:31:42

标签: optimization assembly lisp tail-recursion ml

LISP或ML如何实现尾调用优化?

2 个答案:

答案 0 :(得分:6)

我不能谈论不同的编译器/解释器的确切实现细节,但一般来说尾调用优化的操作如下:

通常函数调用涉及这样的事情:

  1. 为返回分配堆栈空间
  2. 将当前指令指针推入堆栈
  3. 为函数参数分配堆栈空间并相应地设置它们
  4. 致电您的职能
  5. 要返回它会适当地设置它的返回空间,弹出它应该返回的指令指针并跳转到它
  6. 但是当一个函数处于尾部位置时,这意味着你要返回你要调用的函数的结果,你可能会很棘手并且

    1. 重复使用为您自己的返回值分配的堆栈空间,作为为您要调用的函数的返回值分配的堆栈空间
    2. 重新使用您应该返回的指令指针作为您要调用的函数将使用的指令指针
    3. 释放您自己的参数堆栈空间
    4. 为参数分配空间并进行适当设置
    5. 设置参数值
    6. 致电您的职能
    7. 当它返回时,它将直接返回给你的来电者。
    8. 请注意,#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)。

在所有至少可以在寄存器中传递参数的架构中,编译器可以在各种函数上应用尾调用优化。