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

时间:2015-11-22 09:25:55

标签: scala scalac scala-compiler

scala编译器是否通过将refs移除到块中仅使用一次的val来优化内存使用量?

想象一个对象持有一些巨大的数据 - 达到克隆数据或其衍生物可能会刮掉JVM /机器的最大内存量的大小。

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

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

编译器是否例如在计算huge之后,将derivative1标记为是否有资格进行垃圾回收?或者它会保持活着,直到包裹块退出?

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

2 个答案:

答案 0 :(得分: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上的this thread,它目前 执行此操作,最新的热点JVM也不提供抢救。请参阅scalac hacker Grzegorz Kossakowski的帖子以及该主题的其余部分。

对于由JVM JIT编译器优化的方法,JIT编译器将尽快使引用为空。但是,对于仅执行一次的主方法,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的学分。

答案 1 :(得分:2)

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

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

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