我意识到这个问题的答案对于不同的语言可能有所不同,我最感兴趣的语言是C ++。如果标签需要更改,因为无法以语言无关的方式回答,请随意。
<小时/> 是否有可能使一个函数部分尾递归,并且仍然可以获得尾递归的任何优势?
据我了解,尾递归是在不执行完整函数调用的情况下,编译器将优化函数,只是将参数更改为新参数并跳转到函数的开头。
如果你有这样的功能:
def example(arg):
if arg == 0:
return 0 # base case
if arg % 2 == 0:
return example(arg - 1) # should be tail recursive
return 3 + example(arg - 1) # isn't tail recursive because 3 is added to the result
当优化器遇到类似的东西时(在某些情况下函数是尾递归而不在其他情况下),它会将一个转换为jump
而另一个转换为call
,或者将优化现实的一些事实(如果我知道我不会问),它必须将所有内容都变成call
,如果函数是尾递归的话,你将失去所有效率吗?
答案 0 :(得分:1)
在Scheme中,当我想到尾调用时会想到的第一种语言,第二种情况保证是语言规范的尾调用。 (术语说明:最好将此类函数调用称为“尾调用”。)
Scheme规范确切地定义了Scheme中的尾调用,并强制要求编译器专门支持它们。您可以在 11.20中看到该定义。 R 6 RS(source)的尾调用和尾部上下文。
请注意,在Scheme中,规范没有说明尾调用的优化。相反,它表示实现必须支持无限数量的活动尾调用 - 语言运行时的语义属性。它们可以作为普通电话实现,但通常不是。
示例,在C:
以你的例子的C版本为例。
int example(int arg)
{
if (arg == 0)
return 0;
if ((arg % 2) == 0)
return example(arg - 1);
return 3 + example(arg - 1);
}
使用gcc常用的i386优化设置(-O2
)进行编译:
_example:
pushl %ebp
xorl %eax, %eax
movl %esp, %ebp
movl 8(%ebp), %edx
testl %edx, %edx
jne L5
jmp L15
.align 4,0x90
L14:
decl %edx
testl %edx, %edx
je L7
L5:
testb $1, %dl
je L14
decl %edx
addl $3, %eax
testl %edx, %edx
jne L5
L7:
leave
ret
L15:
leave
xorl %eax, %eax
ret
请注意,汇编代码中没有函数调用。 GCC不仅将尾部调用优化为跳跃,还将非尾部调用优化为跳跃。
答案 1 :(得分:0)
据我了解,智能编译器可以通过跳转到示例入口点而不是设置新的堆栈帧来将尾递归应用于第一次调用。以下返回将把堆栈展开到原始调用者,有效地“结束”两个调用,即使它不能为另一个调用执行此操作。
您可以通过在通话中添加3来优化您的功能:
def example(arg, add=0):
arg += add
....
return example(arg - 1, 3) # tail now too
另一种技术是创建第二个函数并同时互相调用。
我不知道python或C ++编译器是否可以处理它,但你可以检查C ++的汇编输出。奇怪的是我认为检查python的字节码输出可能更难。