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


165

Scala的一个方便功能是lazy val,其中a的求值val被延迟到必要时(首次访问时)。

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

-的确切成本是多少lazy val-是否存在与a相关联的隐藏布尔标志lazy val以跟踪是否已被评估,确切地同步了什么,还有更多成本吗?

另外,假设我这样做:

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

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

Answers:


86

这是从scala邮件列表中获取的,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;
  }

}

33
我认为自从该Java版本于2007年发布以来,实现方式一定已经改变。只有一个同步块,并且bitmap$0在当前实现中该字段是可变的(2.8)。
米奇·布列文(Mitch Blevins)2010年

1
是的-我应该更加注意发布的内容!
oxbow_lakes

8
@Mitch- 希望实现有所改变!经过仔细检查的初始化反模式是一个典型的细微错误。参见en.wikipedia.org/wiki/Double-checked_locking
Malvolio 2010年

20
在Java 1.4之前都是反模式。由于Java 1.5 volatile关键字的含义更严格,现在可以进行这种双重检查。
iirekm

8
那么,从scala 2.10开始,当前的实现是什么?另外,请问有人在实践中暗示这意味着多少开销,以及何时使用,何时避免的一些经验法则?
ib84 2013年

39

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


26

使用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。如果添加另一个字段,则编译器将增加该字段的大小,使其能够表示至少两个值,即表示为byte。这只适用于大量课程。

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


11

斯卡拉SIP-20提出了一个新的lazy 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的观察

懒惰的val *不是*免费的(甚至便宜)。仅当您绝对需要懒惰以获得正确性而不是优化时才使用它。



By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.