首页 文章

什么's the (hidden) cost of Scala'懒惰的val?

提问于
浏览
153

Scala的一个便利功能是 lazy val ,其中 val 的评估被推迟到必要时(首次访问时) .

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

lazy val 的成本究竟是什么 - 是否有一个隐藏的布尔标志与 lazy val 相关联,以便跟踪它是否已被评估,究竟是什么同步并且还有更多的成本?

另外,假设我这样做:

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

这是否与两个单独的 lazy val s xy 相同,或者我只获得一次开销,对于 (x, y) 对?

6 回答

  • 9

    这取自scala mailing list,并根据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;
      }
    
    }
    
  • 23

    看起来编译器安排类级别位图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 类型的 x$1 字段 .

  • 38

    使用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 .

  • 11

    Scala SIP-20提出了一个lazy val的新实现,它更正确但比"current"版本慢约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

    Lazy val 免费(甚至便宜) . 只有在绝对需要懒惰才能获得正确性而不是优化时才使用它 .

  • -6

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

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

  • 78

    给出scala为懒惰生成的bycode,它会遇到双重检查锁定http://www.javaworld.com/javaworld/jw-05-2001/jw-0525-double.html?page=1中提到的线程安全问题

相关问题