Scala 的惰性 val 的(隐藏)成本是多少?

Scala 的一个方便的特性是 lazy val,其中对 val的求值被延迟到必要时(在第一次访问时)。

当然,lazy val必须有一些开销—— Scala 必须在某个地方跟踪值是否已经被求值,并且求值必须是同步的,因为多个线程可能会尝试在同一时间首次访问该值。

lazy val的成本到底是多少? 是否有一个隐藏的布尔标志与 lazy val相关联,以便跟踪它是否已经被评估,同步化的内容到底是什么,还有更多的成本吗?

此外,假设我这样做:

class Something {
lazy val (x, y) = { ... }
}

这是否等同于有两个独立的 lazy valsxy,或者对于这对 (x, y),我只得到一次开销?

26711 次浏览

看起来编译器安排了一个类级位图 int 字段来标记初始化(或不初始化)的多个惰性字段,并且如果位图的相关 xor 表明有必要,则在同步块中初始化目标字段。

使用:

class Something {
lazy val foo = getFoo
def getFoo = "foo!"
}

产生样本字节码:

 0  aload_0 [this]
1  getfield blevins.example.Something.bitmap$0 : int [15]
4  iconst_1
5  iand
6  iconst_0
7  if_icmpne 48
10  aload_0 [this]
11  dup
12  astore_1
13  monitorenter
14  aload_0 [this]
15  getfield blevins.example.Something.bitmap$0 : int [15]
18  iconst_1
19  iand
20  iconst_0
21  if_icmpne 42
24  aload_0 [this]
25  aload_0 [this]
26  invokevirtual blevins.example.Something.getFoo() : java.lang.String [18]
29  putfield blevins.example.Something.foo : java.lang.String [20]
32  aload_0 [this]
33  aload_0 [this]
34  getfield blevins.example.Something.bitmap$0 : int [15]
37  iconst_1
38  ior
39  putfield blevins.example.Something.bitmap$0 : int [15]
42  getstatic scala.runtime.BoxedUnit.UNIT : scala.runtime.BoxedUnit [26]
45  pop
46  aload_1
47  monitorexit
48  aload_0 [this]
49  getfield blevins.example.Something.foo : java.lang.String [20]
52  areturn
53  aload_1
54  monitorexit
55  athrow

在类似 lazy val (x,y) = { ... }的元组中初始化的值通过相同的机制嵌套缓存。对元组结果进行延迟计算和缓存,对 x 或 y 的访问将触发元组计算。从元组中提取单个值是独立和惰性的(并缓存)。因此,上面的双实例化代码生成 xyTuple2类型的 x$1字段。

这是从 Scala 邮件列表中获取的,并且根据 Java 代码(而不是字节码)给出了 lazy的实现细节:

class LazyTest {
lazy val msg = "Lazy"
}

被编译成相当于下面的 Java 代码:

class LazyTest {
public int bitmap$0;
private String msg;


public String msg() {
if ((bitmap$0 & 1) == 0) {
synchronized (this) {
if ((bitmap$0 & 1) == 0) {
synchronized (this) {
msg = "Lazy";
}
}
bitmap$0 = bitmap$0 | 1;
}
}
return msg;
}


}

考虑到 scala 为惰性生成的字节码,它可能会遇到双重检查锁定 http://www.javaworld.com/javaworld/jw-05-2001/jw-0525-double.html?page=1中提到的线程安全问题

Scala SIP-20 提出了一个新的惰性 val 实现,它比“当前”版本更正确,但是慢了约25% 。

建议的实施方案看起来像:

class LazyCellBase { // in a Java file - we need a public bitmap_0
public static AtomicIntegerFieldUpdater<LazyCellBase> arfu_0 =
AtomicIntegerFieldUpdater.newUpdater(LazyCellBase.class, "bitmap_0");
public volatile int bitmap_0 = 0;
}
final class LazyCell extends LazyCellBase {
import LazyCellBase._
var value_0: Int = _
@tailrec final def value(): Int = (arfu_0.get(this): @switch) match {
case 0 =>
if (arfu_0.compareAndSet(this, 0, 1)) {
val result = 0
value_0 = result
@tailrec def complete(): Unit = (arfu_0.get(this): @switch) match {
case 1 =>
if (!arfu_0.compareAndSet(this, 1, 3)) complete()
case 2 =>
if (arfu_0.compareAndSet(this, 2, 3)) {
synchronized { notifyAll() }
} else complete()
}
complete()
result
} else value()
case 1 =>
arfu_0.compareAndSet(this, 1, 2)
synchronized {
while (arfu_0.get(this) != 3) wait()
}
value_0
case 2 =>
synchronized {
while (arfu_0.get(this) != 3) wait()
}
value_0
case 3 => value_0
}
}

截至2013年6月,该 SIP 尚未获得批准。我希望它可能会被批准,并包含在基于邮件列表讨论的 Scala 未来版本中。因此,我认为你最好留意 Daniel Spiewak 的观察:

Lazy val 不是免费的(甚至是便宜的)。只有当你绝对的时候才使用它 需要懒惰是为了正确,而不是为了优化。

对于 Scala 2.10,一个惰性值如下:

class Example {
lazy val x = "Value";
}

被编译成类似下列 Java 代码的字节码:

public class Example {


private String x;
private volatile boolean bitmap$0;


public String x() {
if(this.bitmap$0 == true) {
return this.x;
} else {
return x$lzycompute();
}
}


private String x$lzycompute() {
synchronized(this) {
if(this.bitmap$0 != true) {
this.x = "Value";
this.bitmap$0 = true;
}
return this.x;
}
}
}

请注意,位图由 boolean表示。如果添加另一个字段,编译器将增加该字段的大小,使其能够表示至少2个值,即 byte。这种情况只会发生在大型课堂上。

但是你可能想知道为什么会这样?在进入同步块时,必须清除线程本地缓存,以便将非易失性 x值刷新到内存中。这篇博客文章给出了 一个解释

关于这个问题,我已经写了一篇文章 https://dzone.com/articles/cost-laziness

简而言之,惩罚是如此之小,在实践中你可以忽略它。