【问题标题】:What's the (hidden) cost of Scala's lazy val?Scala 的惰性验证的(隐藏)成本是多少?
【发布时间】:2011-03-03 17:53:18
【问题描述】:

Scala 的一个方便的功能是lazy val,其中val 的评估会延迟到需要时(第一次访问)。

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

lazy val 的确切成本是多少 - 是否有与 lazy val 关联的隐藏布尔标志来跟踪它是否已被评估,究竟同步了什么以及是否还有更多成本?

另外,假设我这样做:

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

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

【问题讨论】:

    标签: performance scala lazy-evaluation


    【解决方案1】:

    这取自 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;
      }
    
    }
    

    【讨论】:

    • 我认为自从这个 Java 版本于 2007 年发布以来,实现一定已经改变了。只有一个同步块,bitmap$0 字段在当前实现 (2.8) 中是可变的。
    • 是的 - 我应该更加关注我发布的内容!
    • @Mitch -- 我希望实现已经改变!双重检查初始化反模式是一个经典的微妙错误。见en.wikipedia.org/wiki/Double-checked_locking
    • 它是 Java 1.4 之前的反模式。由于 Java 1.5 volatile 关键字的含义更严格,现在这样的双重检查是可以的。
    • 那么,从 scala 2.10 开始,当前的实现是什么?另外,请有人提示一下这在实践中意味着多少开销,以及何时使用、何时避免的一些经验法则?
    【解决方案2】:

    看起来编译器安排了一个类级别的位图 int 字段以将多个惰性字段标记为已初始化(或未初始化),并在位图的相关异或指示有必要时在同步块中初始化目标字段。

    使用:

    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 将触发元组评估。从元组中提取单个值是独立且惰性地完成的(并缓存)。所以上面的双实例化代码生成了一个xy和一个x$1类型为Tuple2的字段。

    【讨论】:

      【解决方案3】:

      在 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

      【讨论】:

        【解决方案4】:

        Scala SIP-20 提出了惰性验证的新实现,它更正确,但比“当前”版本慢约 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 *不是*免费的(甚至便宜)。只有当你绝对使用它 需要惰性是为了正确,而不是为了优化。

        【讨论】:

          【解决方案5】:

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

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

          【讨论】:

          • 感谢您的基准测试。您还可以对 SIP-20 提议的实施进行基准测试吗?
          【解决方案6】:

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

          【讨论】:

          • 这一说法也是由 mitch 对接受的答案的评论提出的,并被 @iirekm 驳斥:这种模式从 java1.5 开始就很好。
          猜你喜欢
          • 2014-04-30
          • 1970-01-01
          • 1970-01-01
          • 2012-04-27
          • 2010-09-20
          • 2023-03-23
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多