哪些函数可以进行尾调用优化取决于CPU体系结构和/或操作系统。这是因为不同的“调用约定”(用于传递函数参数和/或在函数之间传输控制)在CPU和/或操作系统之间存在差异。通常归结为尾调用中是否必须从堆栈中传递任何内容。例如,考虑以下函数:
void do_a_tailcall(char *message)
{
printf("Doing a tailcall here; you said: %s\n", message);
}
If you compile this, even with high optimization (-O8 -fomit-frame-pointer
), on 32bit x86 (Linux), you get:
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"
i.e. a classical function, with stackframe setup/teardown (subl $12, %esp
/ addl $12, %esp
) and an explicit ret
from the function.
In 64bit x86 (Linux), this looks like:
do_a_tailcall:
movq %rdi, %rsi
xorl %eax, %eax
movl $.LC0, %edi
jmp printf
.LC0:
.string "Doing a tailcall here; you said: %s\n"
so it got tail-optimized.
On an entirely different type of CPU architecture (SPARC), this looks like (I've left the compiler's comment in):
.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
Yet another one ... 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
The differences here are the way arguments are passed, and control is transferred:
32位x86(stdcall
/ cdecl
类型调用)在堆栈上传递参数,因此尾部调用优化的潜力非常有限 - 除了特定的边角情况,只有在精确的参数传递或调用不需要任何参数的函数时,才有可能发生。
64位x86(UNIX x86_64
风格,但在Win64上并没有太大区别)在寄存器中传递一定数量的参数,这使得编译器可以更自由地调用而无需传递任何内容到堆栈。通过jmp
进行控制转移只需使尾部调用的函数继承堆栈 - 包括最顶层的值,它将是原始调用者do_a_tailcall
的返回地址。
SPARC不仅在寄存器中传递函数参数,还传递返回地址(它使用一个链接寄存器,%o7
)。因此,虽然您通过call
进行控制转移,但实际上这并不强制产生新的堆栈帧,因为它只是设置链接寄存器和程序计数器...通过 SPARC 的另一个奇怪特性 - 所谓的延迟槽指令(or %g0,%g1,%o7
- sparc-ish for mov %g1,%o7
- 在call
之后但在达到call
目标之前执行)。上述代码是从旧版本编译器生成的...并不像理论上那样优化...
ARM与SPARC类似,它使用链接寄存器,尾递归函数将其传递未修改/未触及的尾调用。它也类似于x86,通过尾递归使用b
(分支)而不是“call”等效项(bl
,分支和链接)。
在至少一些参数传递可以在寄存器中发生的所有体系结构中,编译器可以对各种函数应用尾调用优化。