【问题标题】:Why languages such as C, Pascal cannot implement tail recursion?为什么C、Pascal等语言不能实现尾递归?
【发布时间】:2015-02-20 04:07:53
【问题描述】:

我正在阅读SICP。在footnote 之一中提到:

让编译器生成尾递归代码似乎是一个简单的想法。但是大多数通用语言的编译器,包括 C 和 Pascal,不这样做,因此这些语言不能仅根据过程调用来表示迭代过程。这些语言中尾递归的困难在于它们的实现使用堆栈来存储过程参数和局部变量以及返回地址。


如果堆栈用于过程参数、局部变量和返回地址,我无法理解为什么不能实现尾递归。

【问题讨论】:

  • 有可能。它只是在生成的过程代码末尾有一个跳转而不是调用,但是语言语义可能必须改变。您将如何处理异常处理? Stacktraces 缺少调用堆栈的部分。等等。这需要很好地定义。

标签: c scheme pascal tail-recursion sicp


【解决方案1】:

C 当然可以实现尾递归。 C 中的尾递归如下所示:

int foo (int bar)
{
    int baz;
    ...
    return foo(baz);
}

所有 C 编译器都可以做到这一点。其中一些(实际上是大多数)为其提供了优化,因此它不使用额外的堆栈空间,例如(gcc、MSVC 和 clang/LLVM):

我对 Pascal 了解不多,但这里引用了 2004 年支持尾递归的基于非 LLVM 的 pascal 编译器:

鉴于 LLVM 案例适用于多种语言,并且可能是最常见的现代编译器后端,并且这些是最常见的 C 编译器,并且鉴于您的源代码似乎在不使用堆栈空间的情况下无法区分尾递归和尾递归,我建议您的来源是错误的,或者充其量是过时的。

关于你问题的第二部分:

如果堆栈用于过程参数、局部变量和返回地址,我无法理解为什么不能实现尾递归。

可以使用堆栈实现尾递归,就像任何其他递归一样。但是,如果您不对其进行优化,以便 使用堆栈(例如上面的链接),那么深度递归将导致您耗尽堆栈空间。可以说,在内存便宜且堆栈大小不受 32 位内存映射限制的现代环境中,这不是问题。然而,鉴于大多数编译器优化并且无论如何都可以避免堆栈,它也适用于其他更具挑战性的环境。

【讨论】:

  • 好的,现代编译器可以进行尾递归,但您能解释一下在使用堆栈实现尾递归时可能遇到的困难吗?来源可能已经过时,但我怀疑它是错误的,因为 SICP 很有名,这样的错误现在已经被纠正了。
  • @AnuragPeshne:查看最近的编辑 - 本质上的困难(没有尾递归优化,因此使用堆栈)是深度递归堆栈溢出的风险。
  • 如果我们不优化,理论上有可能连堆空间都被深度递归耗尽了,对吧?
  • @AnuragPeshne 由于未应用 TCO,堆永远不可能耗尽。当堆栈用完时,操作系统可能会检测到并使用 SIGSEGV 终止进程。
  • @AnuragPeshne 在 linux 上,堆栈增长是有限制的。一旦达到,操作系统将认为堆栈已用完。栈的进一步使用是Undefined Behavior,
【解决方案2】:

这些语言中尾递归的困难在于它们的实现使用堆栈来存储过程参数和局部变量以及返回地址。

在机器语言中,没有函数调用,所以函数调用被翻译成

  1. 将参数推入调用堆栈
  2. 将返回地址压入堆栈
  3. 转到要调用的子例程的主体
  4. 当子程序退出时,它会回到我们之前压入堆栈的返回地址
  5. 参数从堆栈中移除

现在这种“调用约定”模式有两个基本变体,与谁负责第 5 步(从堆栈中删除函数参数)有关。在 C 中,调用约定是函数调用者负责清理堆栈。这允许像printf 这样的可变参数函数(在这些情况下,只有调用者知道调用完成后要弹出的正确参数数量),但这意味着您不能进行尾调用优化,因为“返回”不是最后一件事函数可以(之后您仍然需要清理堆栈)。另一方面,如果您的调用约定是让函数本身清理堆栈,那么您将失去拥有可变参数函数的能力,但可以拥有 TCO。

【讨论】:

猜你喜欢
  • 1970-01-01
  • 2013-02-28
  • 1970-01-01
  • 2019-04-07
  • 1970-01-01
  • 1970-01-01
  • 2011-04-10
  • 2013-09-14
  • 1970-01-01
相关资源
最近更新 更多