Recursion 为什么;没有堆栈溢出之类的事情;吵闹?

Recursion 为什么;没有堆栈溢出之类的事情;吵闹?,recursion,racket,stack-overflow,language-implementation,Recursion,Racket,Stack Overflow,Language Implementation,以下段落摘自: 同时,递归不会导致特别糟糕的结果 在球拍上的表现,并没有堆栈溢出之类的事情; 如果计算涉及太多上下文,可能会耗尽内存, 但是耗尽记忆通常需要更深的数量级 递归将触发其他语言中的堆栈溢出 我很好奇Racket是如何设计来避免堆栈溢出的?更重要的是,为什么像C这样的其他语言无法避免这样的问题?这个答案有两个部分 首先,在Racket和其他函数式语言中,尾部调用不会创建额外的堆栈帧。也就是说,一个循环,例如 (define (f x) (f x)) 。。。可以永远运行而不使用任何堆栈

以下段落摘自:

同时,递归不会导致特别糟糕的结果 在球拍上的表现,并没有堆栈溢出之类的事情; 如果计算涉及太多上下文,可能会耗尽内存, 但是耗尽记忆通常需要更深的数量级 递归将触发其他语言中的堆栈溢出


我很好奇Racket是如何设计来避免堆栈溢出的?更重要的是,为什么像C这样的其他语言无法避免这样的问题?

这个答案有两个部分

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

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

然而,您所指的评论不仅仅限于尾部调用;球拍允许非常深的嵌套堆栈帧

你的问题很好:为什么其他语言不允许深度嵌套的堆栈框架?我编写了一个简短的测试,它看起来像是C随意地将核心转储在262000到263000之间的深度。我编写了一个简单的racket测试来做同样的事情(小心确保递归调用不在尾部位置),我在48000000的深度中断了它,没有任何明显的不良影响(大概除了一个相当大的运行时堆栈)

为了直接回答您的问题,我没有理由知道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 1000000) 0)
    (printf "~v\n" depth))
  (when (< depth 50000000)
    (f (add1 depth)))
  (when (< (random) (/ 1.0 100000))
    (printf "X")))

(f 0)
#朗球拍
(定义(f深度)
(当(=(模深度1000000)0)
(printf“~v\n”深度)
(当深度小于50000000时)
(f(添加1深度)))
(当(<(随机)(/1.0 100000))
(打印F“X”))
(f 0)

此外,我对进程大小的观察结果与大约16字节的堆栈帧一致,正负;50M*16字节=800兆字节,观察到的堆栈大小约为1.2千兆字节

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

下面是一些深层递归(即大上下文)的实现策略:

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

对深层递归的支持通常与对一级连续性的支持一致。一般来说,实现一级延续意味着您几乎可以自动获得对深度递归的支持。威尔·克林格(Will Clinger)等人写了一篇很好的论文,其中详细介绍了不同策略之间的比较。

有趣的是,“智能”编译器可以意识到最后一个
printf
调用是不可访问的,将其完全删除,剩下的是。。。一个尾部递归函数!我们能确定这里没有发生这种情况吗?C堆栈的大小基本上取决于编译器/操作系统。不能指望它,除非在编译期间指定它(假设它在平台上可用,而平台上肯定不需要它)。@WillNess Well。。。我更新了代码,在50米深处,它停止重复并退出,在返回的过程中每100K次调用打印一次输出,效果很好。OTOH,也许聪明的编译器正在优化掉所有不生成输出的调用。。。“我可以用disassembly来确定,”Willenss说,这是另一个实验;我修改了代码,在返回的过程中随机生成一个X,概率为1/100K。同样,它似乎工作正常,这表明球拍在50米的堆叠深度上没有问题。哦!这似乎是确定的,尤其是随机性,肯定并没有编译器会抛弃它。顺便说一句,我毫不怀疑;据说球拍只是把它堆在一堆上,我没有理由不相信这一点。。。也许用这个更新答案?旧的简单代码看起来很可疑/易受影响。:)
#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)