Groovy的trampoline()使得递归执行的速度变慢了-为什么?

时间:2019-06-13 10:56:31

标签: recursion groovy trampolines

我正在尝试递归:

def fac
//fac = { int curr, res = 1G -> 1 >= curr ? res : fac( curr - 1, res * curr ) }
fac = { int curr, res = 1G -> 1 >= curr ? res : fac.trampoline( curr - 1, res * curr ) }
fac = fac.trampoline()

def rnd = new Random()

long s = System.currentTimeMillis()

100000.times{ fac rnd.nextInt( 40 ) }

println "done in ${System.currentTimeMillis() - s} ms / ${fac(40)}"

如果我这样使用它,我会得到的:

  

在691毫秒内完成

如果我取消注释第2行和注释第3-4行以删除trampoline()并运行它,我得到的数字将大大减少:

  

在335毫秒内完成

因此,使用蹦床时,递归的速度要慢2倍。

我想念什么?

P.S。

如果我在Scala 2.12中运行相同的示例:

def fac( curr:Int, acc:BigInt = 1 ):BigInt = if( 1 >= curr ) acc else fac( curr - 1, curr * acc )
val s = System.currentTimeMillis
for( ix <- 0 until 100000 ) fac( scala.util.Random.nextInt(40).toInt )

println( s"done in ${System.currentTimeMillis - s} ms" )

执行速度更快:

  

在178毫秒内完成

更新

将闭包重写为带有注释的方法:

@groovy.transform.TailRecursive
def fac( int curr, res = 1G ) { 1 >= curr ? res : fac( curr - 1, res * curr ) }
// the rest

给予

  

在164毫秒内完成

,是超级大学。不过,我仍然想了解trampoline():)

1 个答案:

答案 0 :(得分:2)

如文档中所述,Closure.trampoline()防止调用堆栈溢出。

  

递归算法通常受到物理限制:最大堆栈高度。例如,如果您调用一个递归调用自身的方法太深,则最终将收到StackOverflowException

     

在这种情况下有用的方法是使用Closure及其蹦床功能。

     

封包包装在TrampolineClosure中。呼叫后,经过抛光处理的Closure将呼叫原始的Closure,等待其结果。如果调用的结果是TrampolineClosure的另一个实例(可能是由于调用trampoline()方法而创建的),则将再次调用Closure。重复执行返回的已抛光的Closures实例,直到返回已抛光的Closure以外的其他值。该值将成为蹦床的最终结果。这样,调用是串行进行的,而不是填充堆栈。

     
     

来源:http://groovy-lang.org/closures.html#_trampoline

但是,使用蹦床需要付费。让我们看一下JVisualVM示例。

非蹦床用例

在没有trampoline()的情况下运行示例,我们在约441毫秒内得到了结果

done in 441 ms / 815915283247897734345611269596115894272000000000

此执行分配了约2,927,550个对象,并消耗了大约100 MB的内存。

enter image description here

CPU有一点工作要做,除了花时间在main()run()方法上之外,它还在强制性参数上花费了一些时间。

enter image description here

trampoline()用例

引进蹦床确实改变了很多。首先,它使执行时间比上一次尝试慢了将近两倍。

done in 856 ms / 815915283247897734345611269596115894272000000000

其次,它分配〜5,931,470 (!!!)对象并消耗约221 MB的内存。主要区别在于,在前一种情况下,所有执行中都使用了一个$_main_closure1,并且在使用蹦床的情况下–对trampoline()方法的每次调用都会创建:

  • 新的$_main_closure1对象
  • 其中包含CurriedClosure<T>
  • 然后将其包裹在TrampolineClosure<T>

仅此分配超过1,200,000个对象。

enter image description here

如果涉及到CPU,它还有很多事情要做。看看数字:

  • 所有对TrampolineClosure<T>.<init>()的呼叫消耗的时间为 199毫秒
  • 使用蹦床会向PojoeMetaMethodSite$PojoCachedMethodSietNoUnwrap.invoke()发出呼叫,这些呼叫总共消耗了额外的 201毫秒
  • 所有对CachedClass$3.initValue()的呼叫总共消耗了另外的 98.8毫秒
  • 所有对ClosureMetaClass$NormalMethodChooser.chooseMethod()的呼叫总共消耗了另外的 100毫秒

enter image description here

这就是为什么在您的案例中引入蹦床会使代码执行慢得多的原因。

那为什么@TailRecursive会好得多?

简而言之-@TailRecursive注释用良好的旧while循环替换了所有闭包和递归调用。在字节码级别,带有@TailRecursive的阶乘函数看起来像这样:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package factorial;

import groovy.lang.GroovyObject;
import groovy.lang.MetaClass;
import java.math.BigInteger;
import org.codehaus.groovy.runtime.ScriptBytecodeAdapter;
import org.codehaus.groovy.runtime.dgmimpl.NumberNumberMultiply;
import org.codehaus.groovy.transform.tailrec.GotoRecurHereException;

public class Groovy implements GroovyObject {
    public Groovy() {
        MetaClass var1 = this.$getStaticMetaClass();
        this.metaClass = var1;
    }

    public static BigInteger factorial(int number, BigInteger acc) {
        BigInteger _acc_ = acc;
        int _number_ = number;

        try {
            while(true) {
                try {
                    while(_number_ != 1) {
                        int __number__ = _number_;
                        int var7 = _number_ - 1;
                        _number_ = var7;
                        Number var8 = NumberNumberMultiply.multiply(__number__, _acc_);
                        _acc_ = (BigInteger)ScriptBytecodeAdapter.castToType(var8, BigInteger.class);
                    }

                    BigInteger var4 = _acc_;
                    return var4;
                } catch (GotoRecurHereException var13) {
                    ;
                }
            }
        } finally {
            ;
        }
    }

    public static BigInteger factorial(int number) {
        return factorial(number, (BigInteger)ScriptBytecodeAdapter.castToType(1, BigInteger.class));
    }
}

前一段时间,我已经在博客中记录了该用例。如果您想获取更多信息,可以阅读博客文章:

https://e.printstacktrace.blog/tail-recursive-methods-in-groovy/