为什么Racket中“没有堆栈溢出这样的东西”?

时间:2018-04-19 03:22:32

标签: recursion racket stack-overflow language-implementation

以下段落来自The Racket Guide (2.3.4)

  

同时,递归并不会导致特别糟糕   在Racket中表现,并没有堆栈溢出这样的东西;   如果计算涉及太多的上下文,你可能会耗尽内存,   但是耗尽内存通常需要更深的数量级   递归比在其他语言中触发堆栈溢出。

我很想知道如何设计Racket以避免堆栈溢出?更何况,为什么像C这样的其他语言无法避免这样的问题?

2 个答案:

答案 0 :(得分:3)

首先,一些术语:进行非尾调用需要上下文框来存储局部变量,父返回地址等。所以问题是如何表示任意大的上下文。 "堆栈" (或调用堆栈)只是上下文的一个(公认的常见)实现策略。

以下是深度递归(即大型上下文)的一些实现策略:

  • 在堆上分配上下文帧,让GC负责清理它们。 (这很简单,但可能相对较慢,尽管人们会认为这一点。)
  • 在堆栈上分配上下文帧。当堆栈已满时,将当前堆栈上的帧复制到堆中,清除堆栈,并将堆栈指针重置为底部。返回时,如果堆栈为空,则将堆中的帧复制回堆栈。 (这意味着你不能指向堆栈分配的对象,因为对象会被移动。)
  • 在堆栈上分配上下文帧。当堆栈已满时,分配一个新的大块内存,调用新堆栈(即设置堆栈指针),并继续运行。 (这可能需要mprotect或其他操作来说服操作系统新的内存块可以作为调用堆栈处理。)
  • 在堆栈上分配上下文帧。当堆栈已满时,创建一个新线程继续计算,并等待线程完成并安排从中获取返回值以返回旧线程的堆栈。 (这种策略在像JVM这样的平台上非常有用,它不允许你直接控制堆栈,堆栈指针等。另一方面,它使线程本地存储等功能变得复杂。)
  • ......以及上述策略的更多变化。

支持深度递归通常与对一流延续的支持相吻合。通常,实现一流的延续意味着您几乎可以自动获得对深度递归的支持。 Will Clinger等人发表了一篇名为this的好文章。更多细节和不同策略之间的比较。

答案 1 :(得分:2)

这个答案有两个部分。

首先,在Racket和其他函数语言中,尾调用不会创建额外的堆栈帧。也就是说,一个循环,如

(define (f x) (f x))

...可以永远运行而不使用任何堆栈空间。许多非函数式语言没有像函数式语言那样优先考虑函数调用,因此没有正确地进行尾调用。

但是,您所指的评论不仅限于尾部呼叫; Racket允许非常深层嵌套的堆栈帧。

您的问题很好:为什么其他语言不允许深层嵌套的堆栈帧?我写了一个简短的测试,看起来C毫不客气地将核心转储到262,000到263,000之间。我写了一个简单的球拍测试做同样的事情(小心确保递归调用不在尾部位置),并且我在48,000,000深度中断它而没有任何明显的不良影响(除了,可能是一个相当大的运行时堆栈) )。

直接回答你的问题,没有理由我知道C不能允许更深层次的嵌套堆栈,但我认为对于大多数C程序员来说,递归深度为262K是足够的。

不适合我们!

这是我的C代码:

#include <stdio.h>

int f(int depth){
  if ((depth % 1000) == 0) {
    printf("%d\n",depth);
  }
  return f(depth+1);
}

int main() {
  return f(0);
}

...和我的球拍代码:

#lang racket

(define (f depth)
  (when (= (modulo depth 1000) 0)
    (printf "~v\n" depth))
  (f (add1 depth))
  (printf "this will never print..."))


(f 0)

编辑:这是在出路上使用随机性阻止可能的优化的版本:

#lang racket

(define (f depth)
  (when (= (modulo depth 1000000) 0)
    (printf "~v\n" depth))
  (when (< depth 50000000)
    (f (add1 depth)))
  (when (< (random) (/ 1.0 100000))
    (printf "X")))

(f 0)

另外,我对进程大小的观察结果与大约16字节的堆栈帧一致,加上或减去; 50M * 16字节= 800兆字节,观察到的堆栈大小约为1.2千兆字节。