【问题标题】:How to predict the maximum call depth of a recursive method?如何预测递归方法的最大调用深度?
【发布时间】:2012-12-09 08:57:01
【问题描述】:

为了估计递归方法在给定内存量下可以实现的最大调用深度,计算在可能发生堆栈溢出错误之前使用的内存的(近似)公式是什么?

编辑:

很多人的回答是“它取决于”,这是合理的,所以让我们通过一个琐碎但具体的例子来删除一些变量:

public static int sumOneToN(int n) {
    return n < 2 ? 1 : n + sumOneToN(n - 1);
}

很容易证明,在我的 Eclipse IDE 中运行它会爆炸,n 不到 1000(对我来说太低了)。 是否可以在不执行的情况下估计此调用深度限制?

编辑:我不禁想到 Eclipse 有一个固定的最大调用深度 1000,因为我得到了998,但是有一个用于主方法,一个用于方法的初始调用,使@987654325 @ 在所有。这是一个“太圆”的数字恕我直言,不是巧合。我会进一步调查。我刚刚 Dux 开销了 -Xss vm 参数;这是最大堆栈大小,因此 Eclipse 运行器必须在某处设置 -Xss1000

【问题讨论】:

  • (这似乎取决于 JVM 和可能的架构。)
  • 这只是局部变量+帧指针+返回地址。
  • 我也觉得不测量是无法估计的
  • 递归实际上与此无关(除了它是生成非常深的调用堆栈的最常用方法)。出于确定调用深度限制的目的,递归调用与任何其他类型的调用没有什么不同。 (当然,编译器可能会消除尾递归,在这种情况下,递归会主动分散您的问题。)

标签: java memory recursion jvm stack-overflow


【解决方案1】:

这显然是 JVM 特定的,也可能是特定于体系结构的。

我测量了以下内容:

  static int i = 0;
  public static void rec0() {
      i++;
      rec0();
  }

  public static void main(String[] args) {
      ...
      try {
          i = 0; rec0();
      } catch (StackOverflowError e) {
          System.out.println(i);
      }
      ...
  }

使用

Java(TM) SE Runtime Environment (build 1.7.0_09-b05)
Java HotSpot(TM) 64-Bit Server VM (build 23.5-b02, mixed mode)

在 x86 上运行。

对于 20MB 的 Java 堆栈 (-Xss20m),每次调用的摊销成本在 16-17 字节左右波动。我见过的最低的是 16.15 字节/帧。因此,我得出结论,成本为 16 个字节,其余为其他(固定)开销。

采用单个int 的函数具有基本相同的成本,16 字节/帧。

有趣的是,一个需要 10 个ints 的函数需要 32 个字节/帧。我不知道为什么成本这么低。

上述结果适用于代码经过 JIT 编译后。在编译之前,每帧的成本要高得多。我还没有找到可靠估计它的方法。 但是,这确实意味着在您能够可靠地预测递归函数是否已被 JIT 编译之前,您没有希望可靠地预测最大递归深度。

所有这些都在ulimit 堆栈大小为 128K 和 8MB 的情况下进行了测试。两种情况下的结果都是一样的。

【讨论】:

  • 你是如何测量递归深度的?
  • 他/她可以使用在每次调用时递增的静态成员字段轻松测量递归深度
  • 在 64 位系统上,每个堆栈帧大约 16 个字节看起来像一个返回地址 + 一个额外地址(帧指针?this 指针(技术上是 null,不缺席)?)
  • @Bohemian:我已经扩展了答案(昨晚没时间做)。
  • @JanDvorak:我希望根本没有this。我只能猜测第二个 8 个字节的用途。很可能是某种指针。
【解决方案2】:

只是部分回答:来自JVM Spec 7, 2.5.2,堆栈帧可以在堆上分配,堆栈大小可能是动态的。我不能肯定地说,但似乎应该可以让您的堆栈大小仅受您的堆大小的限制:

因为除了推送和弹出帧外,从不直接操作 Java 虚拟机堆栈,因此帧可能是堆分配的。

本规范允许 Java 虚拟机堆栈 固定大小或根据需要动态扩展和收缩 计算。如果 Java 虚拟机堆栈的大小是固定的, 可以选择每个 Java 虚拟机堆栈的大小 在创建该堆栈时独立。

Java 虚拟机实现可以为程序员或 用户控制 Java 虚拟机堆栈的初始大小, 以及在动态扩展或收缩 Java 的情况下 虚拟机堆栈,控制最大和最小大小。

所以这取决于 JVM 的实现。

【讨论】:

    【解决方案3】:

    添加到 NPE 答案:

    最大堆栈深度似乎是灵活。以下测试程序会输出大量不同的数字:

    public class StackDepthTest {
        static int i = 0;
    
        public static void main(String[] args) throws Throwable {
            for(int i=0; i<10; ++i){
                testInstance();
            }
        }
    
        public static void testInstance() {
            StackDepthTest sdt = new StackDepthTest();
            try {
                i=0;
                sdt.instanceCall();
            } catch(StackOverflowError e){}
            System.out.println(i);
        }
    
        public void instanceCall(){
            ++i;
            instanceCall();
        }
    }
    

    输出是:

    10825
    10825
    59538
    59538
    59538
    59538
    59538
    59538
    59538
    59538
    

    我使用了这个 JRE 的默认值:

    java version "1.7.0_09"
    OpenJDK Runtime Environment (IcedTea7 2.3.3) (7u9-2.3.3-0ubuntu1~12.04.1)
    OpenJDK 64-Bit Server VM (build 23.2-b09, mixed mode)
    

    所以结论是:如果你的脓够了(即超过两次),你就有第二次机会 ;-)

    【讨论】:

    • 真的很有趣,但我不完全确定在捕获 StackOverflow 后我是否会信任我的虚拟机......我想这会是安全的,但它仍然有点吓人。
    • 整个问题都是关于边缘的东西。我的希望是当 JVM 切换齿轮(从 10k 到 60k)时,有人可以阐明情况。
    • 如果您更改 -Xss 设置,它会更改您获得“低”与“高”输出的次数。我的猜测是 JVM 刚刚进入损坏状态,这就是为什么你不尝试从错误中恢复。
    • @RyanStewart:或者(已经在其他答案中提到):JIT 编译器启动并使用第二种更紧凑的堆栈帧格式。我也不相信 JVM 处于损坏状态,因为这种脱离 JVM 沙箱的方法很容易。并且在JVM检测到所有错误之后。
    • @A.H.我同意。是编译器。我将您的代码更改为运行十次,但在大约 10k 递归时中断,从而避免了溢出。然后再运行一次,直到溢出,它已经达到了 ~60k。
    【解决方案4】:

    答案是,这一切都取决于。

    首先,Java Stack 的大小是可以改变的。

    其次,给定方法的堆栈帧大小可以根据不同的变量而变化。您可以查看Call stack - Wikipedia 的调用堆栈帧大小部分了解更多详细信息。

    【讨论】:

    • 堆栈帧可能是堆分配的,因此帧大小可能无关紧要,具体取决于。
    【解决方案5】:

    取决于您的系统架构(32 位或 64 位地址)、局部变量的数量和方法的参数。 如果是tail-recursive没有开销,如果编译器优化成循环。

    你看,没有简单的答案。

    【讨论】:

    • 这被标记为“java”,所以我们假设我们在这里谈论的是 JVM。
    • @RyanStewart 有 32 位和 64 位 JVM
    • 有,但是JVM中的内存无论如何都是一样的。 JVM 中也没有尾递归,我也不知道有任何 Java 编译器会进行任何此类优化。因此,在我看来,您一定是在谈论其他事情。
    • @Ryan Stewart:我刚刚进行了谷歌搜索,震惊地发现还没有针对 java 的尾递归优化。但可能在未来。
    【解决方案6】:

    您可以走经验主义道路并尝试使用您的代码和-Xss 设置。更多信息请看这里:JVM option -Xss - What does it do exactly?

    【讨论】:

      猜你喜欢
      • 2023-03-15
      • 1970-01-01
      • 2015-06-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-09-24
      • 1970-01-01
      • 2013-12-01
      相关资源
      最近更新 更多