斯卡拉懒惰的val的(隐藏)成本是多less?

Scala的一个便利function是lazy val ,其中lazy val的评估被延迟,直到有必要(在第一次访问时)。

当然,一个lazy val必须有一些开销 – 在某处Scala必须跟踪是否已经评估了该值,并且评估必须同步,因为多个线程可能会同时尝试首次访问该值。

lazy val的成本究竟是什么 – 是否有一个隐藏的布尔标志与一个lazy val跟踪,如果它已经被评估或没有跟踪,什么是同步的,是否有更多的成本?

另外,假设我这样做:

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

这是相同的有两个单独的lazy valxy或我得到的开销只有一次,对(x, y)

这是从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; } } 

它看起来像编译器安排一个类级别位图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) = { ... }初始化的值通过相同的机制嵌套高速caching。 元组的结果被懒惰地评估和caching,并且访问x或者y将触发元组评估。 从元组中提取单个值是独立并且懒惰地(并被caching)完成的。 所以上面的双实例代码生成一个xy和一个types为Tuple2x$1字段。

随着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表示。 如果添加另一个字段,则编译器将增加该字段的大小,以便能够表示至less2个值,即作为一个byte 。 这只是巨大的课程。

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

斯卡拉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版本中。 所以,我认为你应该明白丹尼尔·斯皮瓦克的观点 :

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

我已经写了关于这个问题的posthttps://dzone.com/articles/cost-laziness

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

鉴于scala生成的bycode代码为懒,它可能会遇到线程安全问题,如双重检查locking中提到的http://www.javaworld.com/javaworld/jw-05-2001/jw-0525-double.html?page=1