【问题标题】:Scala compiler optimization for immutabilityScala 编译器优化不变性
【发布时间】:2015-11-22 09:25:54
【问题描述】:

scala 编译器是否通过删除对vals 的引用来优化内存使用,在一个块中只使用一次?

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

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

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

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

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

【问题讨论】:

  • 我不认为编译器可以对 GC(即运行时)做很多事情。检查stackoverflow.com/questions/6275397/…
  • 在答案中解决整体问题而不是(仅)GC限制将非常有帮助......
  • 编译器不会将任何东西标记为“符合垃圾回收条件”。当没有更多对它们的引用时,事物被垃圾收集,而不是编译器指示它。这就是为什么正确答案关于 GC 及其局限性的原因。
  • 那么,以上面一般表达的不可变模式编写的 scala 程序本质上不足以以任何(内存)有效方式处理大数据。您确定取消引用不能以任何方式被字节码暗示吗?如果示例中的代码链是例如拆分成函数?

标签: scala scalac scala-compiler


【解决方案1】:

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

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

【讨论】:

  • 感谢您如此清晰地阐述了问题的要点和细节,并提供了当前最相关的谷歌小组主题!在将其标记为接受之前,我在答案中添加了关于带有迭代器的流计算。 Defs 在other answer 中受到了严厉批评,但不完全确定这种批评对于避免大数据导致内存蔓延的影响有多大...
  • 不完全确定是什么构成了“只执行一次的主要方法”,在这种情况下。我想我们不只是在谈论应用程序的单一主要功能......任何人都可以找到明确定义的好链接?
  • 我严重怀疑附加的类文件在这种情况下是否会产生任何影响。是的,defs 将在每次使用时进行评估。这就是重点。
【解决方案2】:

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

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

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

【讨论】:

  • 前面的答案提到了defs,除了语法和你对这种寿命长的编码风格有什么不同吗?我很乐意在某些情况下采用这种风格……但不一定适用于更长的操作链……
  • defs 是危险的,它们在每次访问时都会重新计算,并且会导致创建额外的 .class 文件。我建议你通过限制范围来采用这种风格。这还有一个额外的好处,就是让程序员的头脑放松,所以他可以确定他已经完成了这个值,并且不会在执行路径的 100 行之后被秘密使用。
  • 很高兴意识到这一点。在较长的数据转换链的一般情况下,这仍然是每个转换一个块。总体目标是在假设垃圾收集的整个转换链中保持内存消耗或多或少地收敛到恒定。引入的每个块都只为一次转换清除了该目标......所以仍然有用,尽管在长链数据转换中的代码风格方面有点可怕。
  • 对了,我为什么要那么在意一些额外的类文件呢? :)
猜你喜欢
  • 2012-09-06
  • 2012-12-19
  • 2011-11-07
  • 1970-01-01
  • 2011-10-05
  • 2019-03-06
  • 2011-11-26
  • 1970-01-01
  • 2014-02-21
相关资源
最近更新 更多