【问题标题】:Can a partially tail-recursive function still gain the optimization advantages of a fully tail-recursive function?部分尾递归函数还能获得完全尾递归函数的优化优势吗?
【发布时间】:2011-08-23 09:28:00
【问题描述】:

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

【问题讨论】:

    标签: optimization language-agnostic partial tail-recursion


    【解决方案1】:

    在Scheme中,当我想到尾调用时,我想到的第一种语言,语言规范保证第二种情况是尾调用。 (术语说明:最好将此类函数调用称为“尾调用”。)

    Scheme 规范准确定义了 Scheme 中的尾调用,并要求编译器特别支持它们。您可以在 11.20 中查看定义。 R6RS (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 不仅将尾调用优化为跳转,还将非尾调用优化为跳转。

    【讨论】:

      【解决方案2】:

      据我了解,智能编译器可以通过跳转到示例入口点而不是设置新的堆栈帧来将尾递归应用于您的第一次调用。随后的返回会将堆栈展开到原始调用者,有效地在一个步骤中“结束”两个调用,即使它不能对另一个调用执行此操作。

      您可以通过在调用中添加 3 来优化您的功能:

      def example(arg, add=0):
          arg += add
          ....
          return example(arg - 1, 3)  # tail now too
      

      另一种技术是创建第二个函数并让两者相互调用。

      我不知道 python 或 C++ 编译器是否可以处理这个问题,但您可以检查 C++ 的程序集输出。奇怪的是,我认为检查 python 的字节码输出可能更难。

      【讨论】:

      • 是的,我知道我可以在无条件尾递归的地方更改它,但我想举一个例子来说明我在说什么。是的,我很确定 C++(至少是 MSVC++)可以将相互递归的函数转换为尾调用。
      • CPython 实现从不 优化了尾调用,Guido 已经涉足该主题,称其为“unpythonic”。见neopythonic.blogspot.com/2009/04/…
      • @Dietrich 是的,当我尝试应该是尾递归阶乘函数时,我注意到了这一点 :) 太糟糕了 guido 太短视了。
      • @Seth:请不要这么粗鲁地谈论语言设计师。尾调用优化需要权衡取舍,这不是一个显而易见的选择。
      猜你喜欢
      • 2010-10-20
      • 2018-03-08
      • 2017-05-23
      • 2017-04-18
      • 1970-01-01
      • 1970-01-01
      • 2012-03-04
      • 2012-10-14
      相关资源
      最近更新 更多