为什么像C、Pascal这样的语言无法实现尾递归?

7 浏览
0 Comments

为什么像C、Pascal这样的语言无法实现尾递归?

我正在阅读《计算机程序的构造和解释》。其中一篇脚注提到:

“使编译器生成尾递归代码可能看起来很直接。但是,包括C和Pascal在内的大多数通用语言编译器并不这样做,因此这些语言无法仅通过过程调用来表示迭代过程。这些语言中尾递归的困难在于它们的实现使用堆栈来存储过程参数、局部变量和返回地址。”

我无法理解为什么在使用堆栈存储过程参数、局部变量和返回地址的情况下不能实现尾递归。

0
0 Comments

为什么像C、Pascal这样的语言无法实现尾递归?

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

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

  1. 将参数推入调用栈
  2. 将返回地址推入栈中
  3. 跳转到要调用的子程序主体
  4. 当子程序退出时,返回到先前推入栈中的返回地址
  5. 参数从栈中移除

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

关于这一点的详细说明可以在这里找到 - drdobbs.com/tackling-c-tail-calls/184401756 - 包括对gcc(或clang)可以优化的尾调用的“兄弟调用”进行描述。

另一个很好的参考资料是经典的"Lambda the ultimate GOTO" 论文。

0
0 Comments

为什么像C、Pascal这样的编程语言不能实现尾递归?

C语言确实可以实现尾递归。在C语言中,尾递归的实现方式如下:

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

所有的C编译器都可以实现这种尾递归。其中大多数编译器会对其进行优化,以节省额外的堆栈空间,例如gcc、MSVC和clang/LLVM。

至于Pascal语言,我对它了解有限,但是有一个基于非LLVM的Pascal编译器在2004年就支持尾递归。

考虑到LLVM适用于多种语言并且是最常见的现代编译器后端,而且这些都是最常见的C编译器,还有你的来源似乎没有区分使用堆栈空间和不使用堆栈空间的尾递归,我认为你的来源要么是错误的,要么至少是过时的。

至于你问题的第二部分:

“我无法理解为什么在使用堆栈作为过程参数、局部变量和返回地址的情况下无法实现尾递归。”

实际上,可以使用堆栈来实现尾递归,就像其他递归一样。然而,如果你没有对其进行优化,使其不使用堆栈(例如上面提到的链接),那么深递归会导致堆栈空间不足。在一个现代环境中,内存便宜且堆栈大小不受32位内存映射的限制,这可能不是一个问题。然而,考虑到大多数编译器都会进行优化并且可以避免使用堆栈,即使在其他更具挑战性的环境中也能正常工作。

好的,现代编译器可以实现尾递归,但是你能解释一下在使用堆栈时实现尾递归可能面临的困难吗?尽管来源可能过时,但我怀疑它是错误的,因为SICP很有名,这样的错误早就会被纠正了。

回答:请参考最近的编辑内容-在不优化尾递归(即使用堆栈)的情况下,深递归会导致堆栈溢出的风险。

如果我们不进行优化,理论上可能会由于深递归而耗尽堆空间,对吗?

不进行尾递归优化是不可能耗尽堆空间的。当堆栈用完时,操作系统可能会检测到并终止具有SIGSEGV信号的进程。

在Linux上,堆栈的增长是有限制的。一旦达到限制,操作系统会认为堆栈已经用完。进一步使用堆栈就会产生未定义行为。

0