web-dev-qa-db-ja.com

Scalaのlazy valの(隠された)コストはいくらですか?

Scalaの便利な機能の1つはlazy valで、valの評価は必要になるまで(最初のアクセスで)遅延されます。

もちろん、lazy valにはオーバーヘッドが必要です-複数のスレッドがアクセスしようとする可能性があるため、どこかScalaは値が既に評価され、評価が同期されているかどうかを追跡する必要があります同時に最初の値。

lazy valの正確なコストは何ですか?-評価されているかどうかを追跡するためにlazy valに関連付けられた隠しブール値フラグはありますか?

さらに、私がこれを行うと仮定します:

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

これは、2つの別個のlazy vals xyを持っていることと同じですか、または(x, y)ペアに対して一度だけオーバーヘッドを取得しますか?

160
Jesper

これは 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;
  }

}
80
oxbow_lakes

コンパイラは、クラスレベルのビットマップ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のいずれかのアクセスはタプル評価をトリガーします。 Tupleからの個々の値の抽出は、独立して遅延して行われます(そしてキャッシュされます)。したがって、上記の二重インスタンスコードは、xy、および_x$1_型の_Tuple2_フィールドを生成します。

38
Mitch Blevins

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値がメモリにフラッシュされるように、同期ブロックに入るとき、スレッドローカルキャッシュをクリアする必要があります。このブログ記事では 説明 を提供しています。

24

Scala SIP-2 は、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に基づいて承認され、含まれると思われます結果として、あなたは注意するのが賢明だと思います ダニエル・スピーワクの観察

Lazy valは無料ではありません(または安価です)。最適化のためではなく、正確性のために絶対に怠absolutelyが必要な場合にのみ使用してください。

11
Leif Wickland

この問題に関する投稿を書いた https://dzone.com/articles/cost-laziness

一言で言えば、ペナルティは非常に小さいため、実際には無視できます。

9
Roman