Berapa biaya (tersembunyi) dari val malas Scala?

165

Salah satu fitur praktis dari Scala adalah lazy val, di mana evaluasi a valditunda sampai diperlukan (pada akses pertama).

Tentu saja, lazy valharus memiliki beberapa overhead - di suatu tempat Scala harus melacak apakah nilai telah dievaluasi dan evaluasi harus disinkronkan, karena beberapa utas mungkin mencoba mengakses nilai untuk pertama kalinya pada waktu yang sama.

Apa sebenarnya biaya a lazy val- apakah ada bendera boolean tersembunyi yang terkait dengan lazy valuntuk melacak apakah telah dievaluasi atau tidak, apa sebenarnya yang disinkronkan dan apakah ada biaya lagi?

Selain itu, misalkan saya melakukan ini:

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

Apakah ini sama dengan memiliki dua lazy vals terpisah xdan yatau apakah saya mendapatkan overhead hanya sekali, untuk pasangan (x, y)?

Jesper
sumber

Jawaban:

86

Ini diambil dari milis scala dan memberikan detail implementasi lazydalam hal kode Java (bukan bytecode):

class LazyTest {
  lazy val msg = "Lazy"
}

dikompilasi dengan sesuatu yang setara dengan kode Java berikut:

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;
  }

}
oxbow_lakes
sumber
33
Saya pikir implementasi pasti sudah berubah sejak versi Java ini diposting pada tahun 2007. Hanya ada satu blok yang disinkronkan dan bitmap$0bidang ini tidak stabil dalam implementasi saat ini (2.8).
Mitch Blevins
1
Ya - Saya seharusnya lebih memperhatikan apa yang saya posting!
oxbow_lakes
8
@Itch - Saya harap implementasinya telah berubah! Anti-pola inisialisasi yang diperiksa ulang adalah bug halus klasik. Lihat en.wikipedia.org/wiki/Double-checked_locking
Malvolio
20
Itu antipernah hingga Jawa 1.4. Karena kata kunci volatil Java 1.5 memiliki makna yang sedikit lebih ketat dan sekarang pemeriksaan ganda seperti itu OK.
iirekm
8
Jadi, pada scala 2.10, apa implementasi saat ini? Juga, dapatkah seseorang memberi petunjuk seberapa besar overhead ini dalam prakteknya dan beberapa aturan praktis kapan harus digunakan, kapan harus dihindari?
ib84
39

Tampaknya kompiler mengatur bidang int bitmap tingkat kelas untuk menandai beberapa bidang malas sebagai diinisialisasi (atau tidak) dan menginisialisasi bidang target dalam blok yang disinkronkan jika xor relevan dari bitmap menunjukkan perlunya.

Menggunakan:

class Something {
  lazy val foo = getFoo
  def getFoo = "foo!"
}

menghasilkan bytecode sampel:

 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

Nilai yang diinisialisasi dalam tupel seperti lazy val (x,y) = { ... }memiliki caching bersarang melalui mekanisme yang sama. Hasil tuple dievaluasi dan di-cache dengan malas, dan akses x atau y akan memicu evaluasi tuple. Ekstraksi nilai individu dari tuple dilakukan secara independen dan malas (dan di-cache). Jadi di atas kode double-Instansiasi menghasilkan x, y, dan x$1bidang jenis Tuple2.

Mitch Blevins
sumber
26

Dengan Scala 2.10, nilai malas seperti:

class Example {
  lazy val x = "Value";
}

dikompilasi ke kode byte yang menyerupai kode Java berikut:

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;
    }
  }
}

Perhatikan bahwa bitmap diwakili oleh a boolean. Jika Anda menambahkan bidang lain, kompiler akan menambah ukuran bidang agar dapat mewakili setidaknya 2 nilai, yaitu sebagai a byte. Ini hanya berlaku untuk kelas besar.

Tetapi Anda mungkin bertanya-tanya mengapa ini berhasil? Cache thread-local harus dihapus ketika memasuki blok yang disinkronkan sehingga nilai non-volatile xdimasukkan ke memori. Artikel blog ini memberikan penjelasan .

Rafael Winterhalter
sumber
11

Scala SIP-20 mengusulkan implementasi baru dari lazy val, yang lebih tepat tetapi ~ 25% lebih lambat dari versi "saat ini".

The pelaksanaan diusulkan terlihat seperti:

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
  }
}

Per Juni 2013 SIP ini belum disetujui. Saya berharap bahwa itu kemungkinan akan disetujui dan dimasukkan dalam versi Scala mendatang berdasarkan diskusi milis. Karenanya, saya pikir Anda sebaiknya memperhatikan pengamatan Daniel Spiewak :

Val malas * tidak * gratis (atau bahkan murah). Gunakan hanya jika Anda benar-benar membutuhkan kemalasan untuk kebenaran, bukan untuk pengoptimalan.

Leif Wickland
sumber
10

Saya telah menulis posting terkait masalah ini https://dzone.com/articles/cost-laziness

Singkatnya, hukumannya sangat kecil sehingga dalam praktiknya Anda bisa mengabaikannya.

Roma
sumber
1
Terima kasih untuk benchmark itu. Bisakah Anda juga melakukan tolok ukur terhadap implementasi SIP-20 yang diusulkan?
Turadg
-6

diberi kode sandi yang dihasilkan oleh scala untuk lazy, ia dapat mengalami masalah keamanan utas seperti yang disebutkan dalam penguncian periksa ganda http://www.javaworld.com/javaworld/jw-05-2001/jw-0525-double.html?page=1

Huy Le
sumber
3
Klaim ini juga dibuat oleh komentar untuk jawaban yang diterima oleh mitch dan dibantah oleh @iirekm: Pola ini baik dari java1.5 dan seterusnya.
Jens Schauder