部分尾递归函数是否仍然可以获得完全尾递归函数的优化优势?

时间:2011-08-23 00:38:15

标签: optimization language-agnostic partial tail-recursion

我意识到这个问题的答案对于不同的语言可能有所不同,我最感兴趣的语言是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,如果函数是尾递归的话,你将失去所有效率吗?

2 个答案:

答案 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的字节码输出可能更难。