【问题标题】:The quickest escape from recursion in Java [duplicate]Java中最快的递归逃脱[重复]
【发布时间】:2015-10-20 02:49:06
【问题描述】:

有没有办法干净而快速地摆脱 Java 中的递归?有一种方法可以使用break; 语句从for loop 中分离出来。是否有等效的模式或方法来转义递归?

我可以考虑生成一个单独的线程,一旦计算出值,就简单地杀死一个线程而不是使递归堆栈冒泡。有没有更好的办法?

已经有一个问题讨论了如何摆脱递归:here

我正在寻找一种更快的方法来实现它,可能无需返回堆栈。类似于gotobreak 声明。

这里考虑的标准是:

  • 使用此转义可轻松重构
  • 原始性能(越快越好)
  • 实践中的长度(写/添加越快越好)

我正在寻找的答案将解释解决方案的性能和简单性 - 这是在算法竞争的背景下提出的,因此首选需要较少重构的解决方案。

我为什么要使用它?

有时在为一些算法竞赛编码时,您需要从递归内部返回一个值,我想知道您是否可以通过使用这种中断更快地做到这一点。想想看起来像这样的算法:

public static MyClass myFunct(MyClass c, int x){
    myFunct(c, c.valueA);
    myFunct(c, c.valueB);
    //do some work - that modifies class c
    if(c.valueE == 7){
        //finished work, can return without unwinding the whole recursion
        //No more modifications to class c
    }
    myFunct(c, c.valueC);
    myFunct(c, c.valueD);
    return c;
}

【问题讨论】:

  • 这里有一个很好的讨论:stackoverflow.com/questions/856124/…
  • throw new ResultFoundException(...); 虽然对于否决票,我不会冒险将其作为答案。 :)
  • @JoopEggen cmets 不能被否决。虽然这既不是 op 想要的,也不是一个好的或至少可以接受的解决方案。这也需要回退堆栈跟踪 - 这是 op 明确想要做的 - 并且引发异常通常是一种不好的风格。
  • 我认为这实际上是第一条评论中链接的问题的副本,但我总是对关闭或使用 dupehammer 犹豫不决......
  • 通常是递归方法决定递归或评估一个值,因此break 将是return <value>; 而不是return recur(...);。如果您正在遍历一棵树,如果当前的孩子没有得到答案,您通常会搜索下一个孩子。或许你能想出一个这样行不通的例子?

标签: java algorithm performance recursion


【解决方案1】:

一种选择是在MyClass 上设置一个标志并基于该标志返回:

public static MyClass myFunct(MyClass c, int x){
    if (c.isDoneCalculating) {
        return c;
    }
    myFunct(c, c.valueA);
    myFunct(c, c.valueB);
    //do some work - that modifies class c
    if(c.valueE == 7){
        c.isDoneCalculating = true;
        //finished work, can return without unwinding the whole recursion
        //No more modifications to class c
    }
    myFunct(c, c.valueC);
    myFunct(c, c.valueD);
    return c;
}

【讨论】:

    【解决方案2】:

    你的问题的答案很简单:

    只需按照通常的方式进行,即使用returns 自己展开堆栈。为什么?因为这并不像你想象的那么慢。除非递归中的计算非常简单并且堆栈深度非常高,否则返回永远不会显着影响算法的运行时间。

    基本上,您有以下选择:

    • 如果可能,请将您的算法转换为迭代算法。
    • 将您的算法转换为结束递归,并希望 VM 将重用堆栈帧。那么,递归返回几乎等于一个简单的返回。
    • 抛出异常。但是,这将比返回更​​慢,因为必须构建堆栈跟踪,最终也会遍历堆栈。此外,必须展开堆栈以检查 catches。你什么也没赢。

    前两个选项是可行的,但并不总是可行的。但老实说,别想了。从深堆栈中返回并不是让您的算法变慢的部分。如果您的算法具有非常深的递归,那么无论如何您都会遇到问题(堆栈溢出,递归调用的成本)并且应该考虑重写您的算法。如果堆栈深度较低,那么无论如何这都不是问题。

    这里有一个简单的 Java 测试程序来说明我的意思:

    import java.io.ByteArrayOutputStream;
    import java.io.PrintStream;
    
    public class DeepRecursion {
    
        private long returnStartTime;
    
        public int recurse(int i) {
            int r = (int)Math.sqrt((double)i); // Some non-trivial computation
            if(i == 0) {
                returnStartTime = System.currentTimeMillis();
                return r;
            }
            return r + recurse(i-1);
        }
    
        public void testRecursion(int i, PrintStream p) {
            long startTime = System.currentTimeMillis();
            int result = recurse(i);
            long endTime = System.currentTimeMillis();
    
            p.println(
                    "Recursion depth: " + i + " Result: " + result + "\n" +
                    "Time for recursion" + (returnStartTime - startTime) + "\n" +
                    "Time for return " + (endTime - returnStartTime) + "\n"
                    );
        }
    
        public void testIteration(int i, PrintStream p) {
            long startTime = System.currentTimeMillis();
            int result = 0;
            for(int k = 0; k <= i; k++) {
                int r = (int)Math.sqrt((double)k); // Some non-trivial computation
                result += r;
            }
            long endTime = System.currentTimeMillis();
            p.println("Iteration length: " + i + " Result: " + result + "\nTime: " + (endTime - startTime) );
        }
    
        public static void main(String[] args) {
            DeepRecursion r = new DeepRecursion();
            PrintStream nullStream = new PrintStream(new ByteArrayOutputStream());
    
            for(int k = 0; k < 10; k++) {
                // Test stack depths from 1024 to 33554432
                for(int i = 10; i < 26; i++) {
                    r.testIteration(1 << i, k == 9 ? System.out : nullStream);
                    r.testRecursion(1 << i, k == 9 ? System.out : nullStream);
                }
            }
        }
    }
    

    它计算一个递归函数,其堆栈深度等于输入参数。该函数计算每个堆栈帧中的平方根,以模拟一些非平凡的计算。它还以迭代方式计算相同的函数。为了预热 JIT,程序首先执行 9 次而不打印结果;只打印第十个结果。这是我的结果(我必须使用-Xss1g 将堆栈大小增加到 1 GB。这是我机器上的结果:

    Iteration length: 1024 Result: 21360
    Time for iteration: 0
    Recursion depth: 1024 Result: 21360
    Time for recursion 0
    Time for return 0
    
    Iteration length: 2048 Result: 60810
    Time for iteration: 0
    Recursion depth: 2048 Result: 60810
    Time for recursion 0
    Time for return 0
    
    Iteration length: 4096 Result: 172768
    Time for iteration: 0
    Recursion depth: 4096 Result: 172768
    Time for recursion 0
    Time for return 0
    
    Iteration length: 8192 Result: 490305
    Time for iteration: 0
    Recursion depth: 8192 Result: 490305
    Time for recursion 0
    Time for return 0
    
    Iteration length: 16384 Result: 1390016
    Time for iteration: 0
    Recursion depth: 16384 Result: 1390016
    Time for recursion 0
    Time for return 0
    
    Iteration length: 32768 Result: 3938198
    Time for iteration: 0
    Recursion depth: 32768 Result: 3938198
    Time for recursion 0
    Time for return 0
    
    Iteration length: 65536 Result: 11152256
    Time for iteration: 0
    Recursion depth: 65536 Result: 11152256
    Time for recursion 1
    Time for return 0
    
    Iteration length: 131072 Result: 31570201
    Time for iteration: 0
    Recursion depth: 131072 Result: 31570201
    Time for recursion 1
    Time for return 0
    
    Iteration length: 262144 Result: 89347840
    Time for iteration: 2
    Recursion depth: 262144 Result: 89347840
    Time for recursion 1
    Time for return 1
    
    Iteration length: 524288 Result: 252821886
    Time for iteration: 2
    Recursion depth: 524288 Result: 252821886
    Time for recursion 4
    Time for return 1
    
    Iteration length: 1048576 Result: 715304448
    Time for iteration: 5
    Recursion depth: 1048576 Result: 715304448
    Time for recursion 7
    Time for return 3
    
    Iteration length: 2097152 Result: 2023619820
    Time for iteration: 9
    Recursion depth: 2097152 Result: 2023619820
    Time for recursion 14
    Time for return 4
    
    Iteration length: 4194304 Result: 1429560320
    Time for iteration: 18
    Recursion depth: 4194304 Result: 1429560320
    Time for recursion 29
    Time for return 12
    
    Iteration length: 8388608 Result: -986724456
    Time for iteration: 36
    Recursion depth: 8388608 Result: -986724456
    Time for recursion 56
    Time for return 28
    
    Iteration length: 16777216 Result: -1440040960
    Time for iteration: 72
    Recursion depth: 16777216 Result: -1440040960
    Time for recursion 112
    Time for return 61
    
    Iteration length: 33554432 Result: 712898096
    Time for iteration: 145
    Recursion depth: 33554432 Result: 712898096
    Time for recursion 223
    Time for return 123
    

    如您所见,从一百万深度的堆栈返回需要 3 毫秒。更大的堆栈大小会导致更长的时间,可能是由于堆栈不再适合 L3 缓存。但是,如果您需要如此大的堆栈,则无论如何都会遇到问题,如上所述。以 1 GB 的最大堆栈大小运行 Java 并不是最好的主意。任何低于 131072 的堆栈大小甚至都无法以毫秒为单位进行测量。在一个健全的算法中,堆栈应该比这小得多,所以你应该总是没问题。

    如您所见,最快的解决方案是迭代解决方案,因此如果非常深的递归太慢,请完全避免它,而不是只跳过返回。

    结论

    如果递归对您来说太慢,请完全摆脱它。只是跳过返回不会有很大的不同。

    【讨论】:

    【解决方案3】:

    假设你有一些内部的、重复的递归,比如这个不太明智的代码:

    public A f(B b) {
        C c = new C();
        return recf(b, c);
    }
    
    private A recf(B b, C c) {
        ...
        A a = recf(b2, c2);
        if (a != null) { // found
            return a;
        }
        ...
        return recf(b3, c3);
    }
    

    这将在找到 (a != null) 时产生一系列返回。

    break 的类比是 Throwable。

    public A f(B b) {
        C c = new C();
        try (
            recf(b, c);
            return null; // not found
        } catch (ResultFoundException<A> e) {
            return e.getResult();
        }
    }
    
    private void recf(B b, C c) throws ResultFoundException<A> {
        ...
    }
    
    public class ResultFoundException<A> implements RuntimeException {
        ...
    
        /** Speed-up thanks to James_pic. */
        @Override
        public Throwable fillInStackTrace() { }
    }
    

    【讨论】:

    • 你知道它是否真的执行得更快-我听说异常很重。
    • 例外情况是规则(查找结果的情况)并且会进行堆栈倒带,这有点慢。但是,返回+检查的顺序也会变慢。取决于递归深度。对热点编译器的影响我不知道。用大数据衡量它。
    • 在 HotSpot 上,您可以通过将 fillInStackTrace 中的 ResultFoundException 重写为 return this; 来显着提高性能。大部分异常开销是填充堆栈跟踪的 JNI 调用。
    【解决方案4】:

    一般来说,解决这个问题的最简单方法是用非递归解决方案简单地替换递归。如果实施得当,这也很可能会提高性能。杀死线程的方法相当丑陋 - 我强烈建议不要使用它。但是如果不回退堆栈,就无法摆脱递归。

    递归:

    int rec(int i){
        if(i == condition)
             return i;
    
        //do some stuff with i
    
        return rec(i);
    }
    

    非递归:

    int nonrec(int i){
        Stack<Integer> stack = new Stack<>();
        stack.push(i);
    
        while(!stack.isEmpty())
        {
            Integer v = stack.pop();
    
            //same as above, but no bubbling through the stack
            if(v == condition)
                return v;
    
            //do some stuff with v
    
            stack.push(v);
        }
    
        //undefined, the algorithm hasn't found a solution
        return -1;
    }
    

    这是一个相当简单的例子,但是对于更复杂的递归问题,原理是一样的。

    【讨论】:

    • 我同意这是最明智的做法,但有时工作量很大。我更多地考虑写作速度 - 编码竞赛的东西。
    • @jedrus07 实际上这不应该做太多的工作,因为您需要做的就是使用自己的堆栈而不是系统堆栈。即使在一个方法中有多个递归调用,这也可以非常容易和快速地实现。我将编辑帖子以演示
    • 在您的演示中,您实际上根本不需要堆栈,因为您的 rec 函数是尾递归的 :-)
    【解决方案5】:

    一个被广泛接受的设计——即使它可能取决于你的特定情况——正在使用 Java Future(或类似的)。如果您要使用线程,请记住线程取消需要安全且干净的策略。关于这些主题有很好的文献,例如Java 并发实践

    【讨论】:

      【解决方案6】:

      正如其他人指出的那样,堆栈必须倒带。

      拥有另一个线程很难看,而且可能比抛出异常要慢。

      我会尝试找到一种非递归方法,或者使用适当的返回进行正常递归。也许如果你举一个你正在尝试做的事情的例子,人们可以为你指明正确的方向。

      如果我迫切需要这样做,我将有一个包装方法来捕获递归函数抛出的异常。

      免责声明:我没有尝试过,所以它甚至可能无法编译:)

      class GotAResultException extends Exception {
        private Object theResult;
        public GotAResultException(Object theResult) {
          this.theResult = theResult;
        }
        public Object getResult() {
          return theResult;
        }
      }
      
      Object wrapperMethod(Object blaParameters) {
        try {
          recursiveMethod(blaParameters);
        } catch (GotAResultException e) {
          return e.getResult();
        }
        // maybe return null if no exception was thrown?
        // Your call
        return null;
      }
      
      void recursiveMethod(Object blaParameters) throws GotAResultException {
        // lots of magical code
        // that calls recursiveMethod recursivelly
        // ...
        // And then we found the solution!
        throw new GotAResultException("this is the result");
      }
      

      【讨论】:

      • 我建议使用 EverythingIsFineException 之类的名称,而不是 GotAResultException
      • 为了简洁起见,我会投票给AllIsWellException(并添加了混淆 ;-)(取决于字体))
      【解决方案7】:

      如果您更改算法以维护自己的堆栈而不是使用系统 CPU 堆栈,则可以丢弃堆栈。

      【讨论】:

        猜你喜欢
        • 2019-09-21
        • 2021-09-20
        • 2016-02-19
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-10-31
        相关资源
        最近更新 更多