5

scala 编译器是否通过删除val块中仅使用一次的 s 的引用来优化内存使用?

想象一个对象聚合了一些巨大的数据——达到一个克隆数据或其衍生数据的大小可能会划伤 JVM/机器的最大内存量。

一个最小的代码示例,但想象一个更长的数据转换链:

val huge: HugeObjectType
val derivative1 = huge.map(_.x)
val derivative2 = derivative1.groupBy(....)

编译器是否会在计算后留下huge标记为符合垃圾收集的条件derivative1?还是在退出包装块之前它会保持活动状态?

不变性在理论上很好,我个人觉得它很容易上瘾。但是要适合在当前操作系统上无法逐项进行流处理的大数据对象 - 我会声称它本质上是阻抗与合理的内存利用率不匹配的,因为 JVM 上的大数据应用程序是不是这样,除非编译器针对这种情况进行了优化..

4

2 回答 2

8

首先:只要 JVM GC 认为有必要,就会实际释放未使用的内存。所以 scalac 对此无能为力。

scalac 唯一能做的就是设置对 null 的引用,不仅是在它们超出范围时,而且在它们不再使用时。

基本上

val huge: HugeObjectType
val derivative1 = huge.map(_.x)
huge = null // inserted by scalac
val derivative2 = derivative1.groupBy(....)
derivative1 = null // inserted by scalac

根据scala-internals 上的这个线程,它目前没有这样做,最新的热点 JVM 也没有提供打捞。请参阅 scalac 黑客 Grzegorz Kossakowski 的帖子和该线程的其余部分。

对于 JVM JIT 编译器正在优化的方法,JIT 编译器会尽快将引用设为空。但是,对于只执行一次的 main 方法,JVM 永远不会尝试对其进行完全优化。

上面链接的线程包含对该主题和所有权衡的非常详细的讨论。

请注意,在典型的大数据计算框架(例如 apache spark)中,您使用的值不是对数据的直接引用。所以在这些框架中,引用的生命周期通常不是问题。

对于上面给出的示例,所有中间值都只使用一次。因此,一个简单的解决方案是将所有中间结果定义为 defs。

def huge: HugeObjectType
def derivative1 = huge.map(_.x)
def derivative2 = derivative1.groupBy(....)
val result = derivative2.<some other transform>

一种不同但非常有效的方法是使用迭代器!map像迭代器这样的链接函数filter逐项处理它们,导致没有中间集合被物化..这非常适合场景!这对诸如此类的功能没有帮助,groupBy但可能会显着减少前功能和类似功能的内存分配。上面提到的 Simon Schafer 的学分。

于 2015-11-22T12:05:19.420 回答
2

derivative1一旦超出范围(并且没有其他引用),它将被垃圾收集。为确保尽快发生这种情况,请执行以下操作:

val huge: HugeObjectType
val derivative2 = {
    val derivative1 = huge.map(_.x)
    derivative1.groupBy(....)
}

从代码可读性的角度来看,这也更好,因为很明显' 存在的唯一原因是,并且在右括号之后不再使用它。derivative1derivative2

于 2015-11-22T16:54:46.217 回答