递归函数中栈溢出的原因

Causes of stack overflow in recursive functions

this video 大约 28 分钟时,一位学生问 Brian Harvey 在编写程序时是否应该始终使用迭代过程而不是递归过程。他说没有,因为

Your programs are not gonna run into space limitations. And in terms of locality of what's in memory, you have to have a lot more control than you do over the way the program is interpreted to really affect that.

由于这不是方案课程,我假设他在这里泛泛地谈论编程语言。当他说“"Your programs are not gonna run into space limitations." 时,他是否忽略了堆栈溢出?我对他的回答感到困惑,因为没有堆栈溢出是否意味着您已经 运行 超出了 space 的函数调用?我对 "in terms of locality" 部分一无所知。堆栈溢出可能发生在方案、java 和其他语言中。我是正确的还是我误解了他的说法?

您所指的视频是计算机科学讲座。计算机科学在很大程度上是理论性的,涉及许多与实用无关的计算细节。在这种情况下,正如他在讲座开始时所说的那样,当今的计算机足够大且速度很快,性能很少成为问题。


内存位置与 WhosebugExceptions 无关,在任何语言中。实际上,内存局部性指的是 SRAM(静态 RAM),它保存着总线从内存(可以是磁盘或 RAM)中检索数据时带来的相邻数据的缓存。从此缓存中获取数据比从内存中获取数据更快,因此如果多个连续操作所需的所有数据都在缓存中,程序将 运行 更快。


现在这都是非常低级的。在大多数(如果不是全部)现代语言(如 Java)的背后,都有一个编译器致力于进行大量低级优化。这意味着,首先,您几乎无法在低级别优化代码,尤其是在不干扰编译器优化的情况下。其次,(就像他在你所指的部分之后所说的那样)除非你正在制作资源密集型游戏,否则不值得你花时间担心性能(除非你有明显的性能问题,但更有可能代码中其他问题的指示)。

如今我们巨大的内存堆栈溢出通常是无限递归的标志,就像迭代一个非停止程序是无限循环的标志一样。

所以是的,他是对的。

我的递归过程会导致堆栈溢出吗? 这取决于您设计的递归过程的类型,根据问题,大多数朴素的回归都可以转换为尾调用(在尾调用优化的 'TCO' 语言中),这允许递归到 运行在常量内存中 space,不使用变异或其他有状态的东西。

scheme中,一个迭代过程:

(let ((i 0)
      (max 10))
  (let loop ()
    (cond ((< i max)
           (printf "~A~N" i)
           (set! i (+ i 1))
           (loop))
          (else i))))

此过程使用常量内存,这等于 space 需要在堆栈上存储对循环的调用。这个过程不是一个函数,它使用变异来迭代(它也是一个递归;)但是..)。

scheme中,两次递归:

(define (fact-1 n)
  (cond ((eq? n 1) n)
        (else (* n (fact-1 (- n 1))))))

(define (fact-2 n carry)
  (cond ((eq? n 1) carry)
        (else (fact-2 (- n 1) (* carry n)))))

Fact-1 是一个正常的递归,并且非常实用,没有状态变化,相反,内存使用随着每次 fact-1 调用创建新的词法闭包而增长,最终耗尽堆栈.长得像

=>(fact-1 10)
..(* 10 (fact-1 9))
..(* 10 (* 9 (fact-1 8)))
..(* 10 (* 9 (* 8 (fact-1 7))))
..     .....
..(* 10 (... (* 2 1) ...))
..     .....
..(* 10 362880)
=>3628800

而 Fact-2 是递归的,但采用尾部形式,因此不是构建堆栈,而是在基本情况下折叠调用,而是向前传递值,我们得到这个:

=>(fact-2 10 1)
..(fact-2 9 10)
..(fact-2 8 90)

..(fact-2 7 720)
.. .......(fact-2 1 362880)
=>3628800

这相当于将 fact-1 变成一个交互过程,但没有突变,因为值是向前传递的,而不是赋值。请注意,每次调用仍会产生一个新的词法闭包,但由于函数不会 return 到原始调用者,而是到原始调用者堆栈位置,编译器可以丢弃以前的闭包而不是让它们相互嵌套, 在每个递归级别重新绑定变量。

那么我应该在哪里使用递归与迭代 这完全取决于要设计的流程和使用的语言。如果您的语言不支持 TCO,那么您将只需要使用浅递归,并以有状态的方式编写循环(递归或迭代)过程。如果您确实有 TCO,那么最好使用递归、尾调用、有状态的东西,或者它们的组合。不是所有的递归过程都可以写成尾形式,也不是所有的迭代过程都可以写成递归。如果您关心内存使用情况,并且想要深度递归,则必须使用尾调用。

注意: 你们中的一些人可能已经注意到,但第一个过程实际上也是一个尾调用,但该示例仍然说明了正常迭代执行有状态的要点!事情,并且 运行ning 在恒定的最大内存中,无论所有有效输入如何。