假设我分配了一些大对象(例如大小为N的向量,可能非常大)并对其执行一系列m次操作:
fm( .. f3( f2( f1( vec ) ) ) )
每个都返回一个大小为N的集合。
为简单起见,我们假设每个f都很简单
def f5(vec: Vector[Int]) = { gc(); f6( vec.map(_+1) ) }
因此,vec在每次后续调用时都不再有将来的引用。 (输入f2后,f1' s vec参数永远不会被使用,每次通话都是如此)
但是,由于大多数JVM在堆栈展开(AFAIK)之前不会减少引用,因此我的程序 不需要来消耗NxM内存。通过以下样式进行比较,仅需要2xM(在其他实现中较少)
var vec:Vector[Int] = ...
for ( f <- F ) {
vec = f(vec)
gc()
}
尾递归方法是否存在同样的问题?
这不仅仅是一个学术练习 - 在某些类型的大数据类型问题中,我们可能会选择N,以便我们的程序完全适合RAM。在这种情况下,我应该担心一种流水线方式比另一种方式更可取吗?
答案 0 :(得分:2)
首先,你的问题包含一个严重的误解,以及一个灾难性的错误编码的例子。
但是,因为大多数JVM在堆栈展开之前不会减少引用(AFAIK)......
实际上,没有主流JVM在引用上使用引用计数。相反,它们都使用某种不依赖于引用计数的标记扫描,复制或分代收集算法。
接下来:
def f5(vec: Vector[Int]) = { gc(); f6( vec.map(_+1) ) }
我认为你试图通过gc()
调用“强制”垃圾收集。不要这样做:它非常低效。即使你只是在调查内存管理行为,你很可能会扭曲这种行为,以至于你所看到的并不代表正常的Scala代码。
话虽如此,答案基本上是肯定的。如果您的Scala函数无法进行尾调用优化,那么深度递归可能会导致垃圾保留问题。唯一的“离开”将是JIT编译器能够告诉GC某些变量在方法调用中的特定点处“死”。我不知道HotSpot JIT / GC是否可以做到这一点。
(我想,另一种方法是让Scala编译器明确地将null
分配给死引用变量。但是当你没有垃圾保留问题时,这会产生潜在的性能问题!)< / p>
答案 1 :(得分:2)
添加到@ StephenC的回答
我不知道HotSpot JIT / GC是否可以做到这一点。
热点jit可以在方法中进行活跃度分析,并将局部变量视为无法访问,即使帧仍在堆栈中也是如此。这就是JDK9引入Reference.reachabilityFence的原因,在某些情况下,即使this
在执行该实例的成员方法时也无法访问{。}}。
但是,只有当控制流中没有任何东西仍可以读取该局部变量时,该优化才适用。没有最终阻止或监视退出。所以它取决于scala生成的字节码。
答案 2 :(得分:0)
您示例中的调用是尾调用。他们真的不应该在所有分配的堆栈帧。但是,由于各种不幸的原因,Scala语言规范并未强制要求正确的尾调用,并且出于同样不幸的原因,Scala-JVM实现不执行尾调用优化。
然而,一些JVM具有TCO,例如J9 JVM执行TCO,因此不应该分配任何额外的堆栈帧,一旦下一个尾调用发生就使得中间对象不可达。即使不具有TCO的JVM也能够执行各种静态(逃逸分析,活跃度分析)或动态(逃逸检测,例如Azul Zing JVM这样做)分析,这可能有助于或可能没有帮助情况下。
还有Scala的其他实现:据我所知,Scala.js不执行TCO,但它编译为ECMAScript,从ECMAScript 2015开始,ECMAScript 进行正确的尾调用,因此只要Scala方法调用的编码最终成为ECMAScript函数调用,符合标准的ECMAScript 2015引擎应该消除Scala尾调用。
Scala-native目前不执行TCO,但将来也会执行。