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

针对不变性的Scala编译器优化,scala,scalac,scala-compiler,Scala,Scalac,Scala Compiler,scala编译器是否通过删除块内仅使用一次的vals的引用来优化内存使用 想象一下,一个对象聚集了一些巨大的数据,其大小达到了克隆数据或其派生数据可能会占用JVM/机器的最大内存量的程度 这是一个简单的代码示例,但请想象一个较长的数据转换链: val huge: HugeObjectType val derivative1 = huge.map(_.x) val derivative2 = derivative1.groupBy(....) 在计算了派生1之后,编译器是否将巨型标记为有资格进行

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

想象一下,一个对象聚集了一些巨大的数据,其大小达到了克隆数据或其派生数据可能会占用JVM/机器的最大内存量的程度

这是一个简单的代码示例,但请想象一个较长的数据转换链:

val huge: HugeObjectType
val derivative1 = huge.map(_.x)
val derivative2 = derivative1.groupBy(....)
在计算了
派生1
之后,编译器是否将
巨型
标记为有资格进行垃圾收集?或者在包装块退出之前,它会保持它的活动状态吗


不变性在理论上是很好的,我个人觉得它让人上瘾。但为了适合当前操作系统上无法逐项流式处理的大数据对象,我认为它与合理的内存利用率天生不匹配,因为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
根据scala内部的说法,它目前没有做到这一点,最新的热点JVM也没有提供补救。请参阅scalac黑客Grzegorz Kossakowski的帖子和其他帖子

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

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

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

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

def huge: HugeObjectType
def derivative1 = huge.map(_.x)
def derivative2 = derivative1.groupBy(....)
val result = derivative2.<some other transform>
def巨型:HugeObject类型
def DEVIDERITIVE1=巨大的.map(ux)
def DEVISTATIVE2=DEVISTATIVE1.groupBy(..)
val结果=导数2。

一种不同但非常有效的方法是使用迭代器!迭代器上的链接函数,如
map
filter
,逐项处理这些函数,从而不会实现任何中间集合。。这非常适合这个场景!这对像
groupBy
这样的函数没有帮助,但可能会显著减少前一个函数和类似函数的内存分配。上面提到的Simon Schafer的功劳。

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

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

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

我认为编译器不能对GC(即运行时)做很多工作。检查在答案中解决整个问题而不是(仅)GC限制将非常有帮助……编译器不会标记任何“符合垃圾收集条件”的内容。当不再有对它们的引用时,它们被垃圾收集,而不是由编译器指示它进行垃圾收集。这就是为什么正确的答案是关于GC及其局限性的。因此,以上面通常表示的不变模式编写的scala程序本质上不足以以任何(内存)高效的方式处理大数据。您确定字节码不能以任何方式暗示取消引用吗?如果示例中的代码链被拆分为函数会怎样?前面的答案提到了def,除了语法和您对这种寿命很长的编码风格的理解之外,还有什么区别吗?我很乐意在某些情况下采用这种风格。。。虽然对于较长的操作链不一定如此,..def是危险的,但它们在每次访问时都会重新计算,并导致创建额外的.class文件。我建议你通过限制范围来采用这种风格。这还有一个额外的优点,就是让程序员的思维更加轻松,这样他就可以确保自己已经完成了这个值,并且不会在执行过程中被秘密使用100行。很高兴认识到这一点。不过,在数据转换链较长的一般情况下,每个转换一个块。总的目标是在整个转换链中,保持内存消耗或多或少地趋于恒定,假定垃圾收集。引入的每个块只为一次转换清除该目标,然后。。。所以仍然很有用,尽管在长数据转换链中的代码样式有点可怕。顺便问一下,为什么我真的那么关心几个额外的类文件呢?:)感谢您如此清晰地阐述了问题的要点和细节,并提供了当前最相关的google group主题!在将其标记为接受之前,我在答案中添加了关于使用迭代器进行流计算的内容。DEF在过去几年受到了严厉的批评,但不完全确定这种批评在避免大数据造成内存泛滥方面有多重要……不完全确定在这种情况下,什么构成“只执行一次的主要方法”。我想我们讨论的不仅仅是应用程序的单一主功能。。。有没有关于最终定义的好链接?我严重怀疑附加的类文件在这种情况下会有什么不同。是的,每次使用时都会对DEF进行评估。那不是