【问题标题】:Recursion runtime implementation Java vs. other/functionals languages?递归运行时实现Java与其他/功能语言?
【发布时间】:2011-04-18 05:23:41
【问题描述】:

我喜欢递归,但在 Java 中,您有时会遇到死胡同。例如。我有一个案例,大约 100K 迭代的递归不起作用(StackOverflowError)。糟糕的是,由于运行时堆栈限制的原因,我不得不切换到烦人的“命令式循环”。

我想知道其他(尤其是函数式)语言如何在运行时绕过堆栈溢出?我想特别是函数式语言运行时可以更好地处理这个问题,因为递归是核心概念......

有人有一些信息或外部资源吗?

【问题讨论】:

标签: java programming-languages recursion functional-programming


【解决方案1】:

大多数语言都有针对tail recursion 的编译器优化。尾递归意味着递归调用应该是递归方法的最后一次调用。然后编译器可以将其优化为循环,防止发生堆栈溢出错误。

我不确定是否所有javac 实现都实现了尾递归。这不是规范所要求的。但是,对于任何递归程序来说,它都是一种重要的优化技术,所以我想主流的实现确实提供了尾递归。

您可以通过一个(简单的)非尾递归程序自行测试,该程序生成一个StackOverflowError 并使其成为尾递归(例如,计算一个factorial)。

编辑:如用户 sje397 的评论所示,Java 中以前有一个 question about tail recursion。还可以查看Stephen C's answer 这个问题,它提供了一些额外的信息。

【讨论】:

  • 我从来没有听说过java编译器优化了尾递归。但是 scala 编译器可以。
【解决方案2】:

这是@Ronald Wildenberg 回答的后续:

我不确定是否所有 javac 实现都实现了尾递归。这不是规范所要求的。

简短的回答是他们不支持它。

更长的答案是,由于 JVM 设计,尾递归优化在 Java 中是一个棘手的问题。阅读 John Rose @ Oracle 的 this blog entry,他谈到了这个问题。这篇博文的主旨是提议一个字节码扩展来支持“硬”尾调用。但最后一段暗示了为什么实现“软”(即透明)尾调用很难。尾调用优化会干扰 JVM 捕获堆栈跟踪的能力,这对 Java 安全架构有“影响”。

Sun Bug Database Entry 提供了有关该问题的更多详细信息。同时阅读 cmets。

看来目前JVM上实现尾递归的方式是在编译器前端实现。显然 Scala 就是这样做的。

【讨论】:

    【解决方案3】:

    “约 10 万次迭代的递归”是应该避免的,不仅仅是在 Java 中。它仅由于尾调用优化而起作用,这并不总是可能的。所以最好不要一开始就养成过度递归的习惯。

    递归是人们在第一次学习时倾向于过度使用的概念之一,因为它看起来太酷了,不能到处炫耀......

    【讨论】:

    • 我同意,递归并不是所有问题的解决方案,我只是想知道语言运行时如何解决大数据。阅读答案尾递归是唯一的优化。您说尾递归并不总是可能的,这意味着您在面对更大的数据集时通常必须更改算法。
    • @manuel: 最好的/有用的递归算法会做某种分而治之的事情,它将最大递归深度限制为 log(n),这意味着它实际上不会成为问题(假设单个递归步骤不需要太多额外的内存)。
    • '“大约 10 万次迭代的递归”是应该避免的,而不仅仅是在 Java 中。嗯,在递归是唯一迭代方式的语言中很难避免。但当然,这些语言保证尾递归得到优化——你只需要确保你的函数是尾递归的。
    • “10 万次迭代的递归”本质上没有任何问题,除了无法处理它的语言。有许多算法在递归中表达得更清楚。一种语言不以这种方式表达它们的能力是该语言的一种限制(尽管很多时候是出于完全正当的理由)。对于这些语言,通常有一些方法可以利用它们通过放弃处理此类问题的能力获得的收益来解决问题。
    【解决方案4】:

    您可以使用以下方法增加 java 堆栈大小:

    java -Xss1024k MyProgram
    

    (将 java 的堆栈大小增加到 1024 KB。) 但通常使用这种深度递归并不是一个好主意。尝试迭代求解。

    【讨论】:

    • 感谢参数提示。但我不会用它来绕过核心大堆栈问题。
    【解决方案5】:

    正如其他人所提到的,支持适当的尾递归会有所帮助。但仅靠它本身还不足以支持深度递归。尽管 BLUB 程序员可能会这么想,但深度递归非常适合某些任务,例如处理深度递归数据结构。

    支持深度(非尾)递归的策略通常包含在支持一流延续的策略中。你可能会觉得Implementation Strategies for First-Class Continuations(Clinger 等人)很有趣。

    我想到了一些策略:

    • CPS 转换您的程序或以其他方式堆分配延续帧(缺点:失去堆栈的一些性能优势)
    • 在堆栈溢出时,将堆栈复制到单独的内存块并将堆栈指针重置为基址;在下溢时,将旧堆栈复制回
    • 在堆栈溢出时,从堆中分配一个新的堆栈区域并在那里重置堆栈指针;在下溢时,返回旧堆栈的末尾
    • 在堆栈溢出时,创建一个新线程继续运行计算,并等待线程的结果(缺点:创建线程可能很昂贵(但工作线程可以被缓存),并且跨线程移动计算可能会导致 havok使用线程本地存储等)

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-03-09
      • 1970-01-01
      • 1970-01-01
      • 2013-10-30
      • 1970-01-01
      • 2015-02-20
      • 1970-01-01
      • 2016-06-10
      相关资源
      最近更新 更多