针对不可变性的 Scala 编译器优化

Scala compiler optimization for immutability

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

想象一个对象持有一些聚合的巨大数据 - 达到一个大小,克隆数据或它的衍生物很可能会划伤 JVM/machine 的最大内存量。

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

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

编译器是否会,例如?在 derivative1 计算完成后,将 huge 标记为有资格进行垃圾回收?或者它会在退出包装块之前一直保持活动状态吗?

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

首先:只要 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

根据this thread on scala-internals,它目前这样做,最新的热点JVM也不这样做提供救助。请参阅 scalac 黑客 Grzegorz Kossakowski 的 post 和该线程的其余部分。

对于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>

一种不同但非常有效的方法是使用迭代器!在迭代器上链接 mapfilter 之类的函数逐项处理它们,导致没有任何中间集合被具体化..这非常适合这个场景!这对 groupBy 这样的函数没有帮助,但可能会显着减少以前函数和类似函数的内存分配。上述内容归功于 Simon Schafer。

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

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

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