Scalaのレイジーバリューの(隠された)コストはいくらですか?


165

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

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

正確には何のコストですか?評価されているかどうかを追跡するためににlazy val関連付けられている非表示のブールフラグlazy valはありますか?正確に同期されているものはありますか?それ以上のコストはありますか?

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

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

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

回答:


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)では、同期ブロックは1つしかなく、フィールドは揮発性です。
Mitch Blevins、2010年

1
はい-投稿した内容にもっと注意を払うべきでした!
oxbow_lakes 2010年

8
@ミッチ- 実装が変更されたことを願っています!ダブルチェックされた初期化アンチパターンは、古典的な微妙なバグです。en.wikipedia.org/wiki/Double-checked_locking
Malvolio

20
Java 1.4まではアンチパターンでした。Java 1.5のvolatileキーワードは少し厳密な意味を持っているため、このような二重チェックは問題ありません。
iirekm 2011

8
では、Scala 2.10では、現在の実装は何ですか?また、誰かがこれが実際にどのくらいのオーバーヘッドを意味するのか、いつ使用するべきか、いつ避けるべきかについての経験則をヒントにしてくれませんか?
ib84 2013年

39

コンパイラーは、クラスレベルのビットマップ整数フィールドを初期化済み(またはそうでない)として複数の遅延フィールドにフラグを立てるように調整し、ビットマップの関連する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のいずれかのアクセスによりタプル評価がトリガーされます。タプルからの個々の値の抽出は、独立して遅延して(そしてキャッシュされて)行われます。上記の二重インスタンス化コードが生成のでxy、とx$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。別のフィールドを追加すると、コンパイラーはフィールドのサイズを増やして、少なくとも2つの値、つまりを表すことができるようになりますbyte。これは、巨大なクラスでも同様です。

しかし、なぜこれが機能するのか疑問に思うかもしれません。スレッドローカルキャッシュは、不揮発性のx値がメモリにフラッシュされるように、同期ブロックに入るときにクリアする必要があります。このブログ記事で説明します。


11

Scala SIP-20はレイジーヴァルの新しい実装を提案しています。これはより正確ですが、「現在の」バージョンよりも約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の観察に耳を傾けるのは賢明だと思います。

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



-6

遅延のためにscalaによって生成されたbycodeを考えると、ダブルチェックロックhttp://www.javaworld.com/javaworld/jw-05-2001/jw-0525-double.html?page=1で言及されているように、スレッドセーフの問題が発生する可能性があります


3
この主張は、ミッチが承認した回答に対するコメントによっても作成され、@ iirekmによって反論されました。このパターンは、java1.5以降で問題ありません。
Jens Schauder
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.