2010-06-14 83 views
145

斯卡拉的一个便捷功能是lazy val,其中val的评估延迟到必要时(首次访问时)。斯卡拉懒惰val的(隐藏)成本是多少?

当然,一个lazy val必须具有一些开销 - 地方斯卡拉必须跟踪的该值是否已经被评估和评价必须是同步的,因为多个线程可能会试图同时访问该值首次时间。

lazy val的成本究竟是什么 - 是否存在一个与lazy val相关的隐藏布尔标志,用于跟踪它是否已经被评估,什么是同步的,是否还有更多的成本?

此外,假设我这样做:

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

这是就等于拥有两个独立的lazy val小号xy还是我得到的开销只有一次,一对(x, y)

回答

77

这是从scala mailing list拍摄,并给出了lazy实施细则中的Java代码(而不是字节码):由

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; 
    } 

} 
+30

我认为自2007年Java版本发布以来,实现必须发生变化。只有一个同步块,位图$ 0字段在当前实现中是不稳定的(2.8)。 – 2010-06-15 18:09:42

+1

是的 - 我应该更多地关注我发布的内容! – 2010-06-16 09:25:34

+0

所以,基本上,这意味着首次访问懒惰值比直接值慢(甚至可能在奇怪的情况下创建死锁),但后续访问几乎不会慢于非惰性值。看起来这不是轻率的,只是用于真正昂贵的初始化。 – 2010-11-04 09:36:31

38

它看起来像编译器安排类级别位图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的访问将触发元组评估。独立并且懒惰地(并且缓存)从元组中提取单个值。因此,上述双实例代码生成x,yx$1类型的字段Tuple2

11

Scala SIP-20提出了一个新的lazy val实现,它比“当前”版本更正确,但速度要慢25%。

proposed implementation样子:

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's observation

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

22

使用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值被刷新到内存中。这篇博客文章给出了an explanation

8

我写了一个帖子关于这个问题https://dzone.com/articles/cost-laziness

在概括地说,惩罚是如此之小,实际上,你可以忽略它。

+1

感谢您的基准。你还可以基于SIP-20提出的实现进行基准测试吗? – Turadg 2015-01-29 17:29:09