Was kostet Scalas fauler Val (versteckt)?


165

Eine praktische Funktion von Scala ist lazy val, dass die Auswertung von a valverzögert wird, bis es notwendig ist (beim ersten Zugriff).

Natürlich lazy valmuss ein gewisser Overhead auftreten - irgendwo muss Scala nachverfolgen, ob der Wert bereits ausgewertet wurde, und die Auswertung muss synchronisiert werden, da möglicherweise mehrere Threads gleichzeitig versuchen, zum ersten Mal auf den Wert zuzugreifen.

Was genau kostet a lazy val- gibt es ein verstecktes boolesches Flag, das mit a verknüpft ist lazy val, um den Überblick zu behalten, ob es ausgewertet wurde oder nicht, was genau synchronisiert ist und ob weitere Kosten anfallen?

Angenommen, ich mache Folgendes:

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

Ist dies dasselbe wie zwei separate lazy vals xund yoder bekomme ich den Overhead nur einmal für das Paar (x, y)?

Antworten:


86

Dies ist der Scala-Mailingliste entnommen und enthält Implementierungsdetails lazyin Bezug auf Java-Code (anstelle von Bytecode):

class LazyTest {
  lazy val msg = "Lazy"
}

wird zu etwas kompiliert, das dem folgenden Java-Code entspricht:

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
Ich denke, die Implementierung muss sich geändert haben, seit diese Java-Version im Jahr 2007 veröffentlicht wurde. Es gibt nur einen synchronisierten Block und das bitmap$0Feld ist in der aktuellen Implementierung volatil (2.8).
Mitch Blevins

1
Ja - ich hätte mehr auf das achten sollen, was ich gepostet habe!
oxbow_lakes

8
@Mitch - Ich hoffe die Implementierung hat sich geändert! Das doppelt überprüfte Anti-Pattern für die Initialisierung ist ein klassischer subtiler Fehler. Siehe en.wikipedia.org/wiki/Double-checked_locking
Malvolio

20
Es war Antipattern bis Java 1.4. Da das flüchtige Schlüsselwort Java 1.5 eine etwas strengere Bedeutung hat und eine solche doppelte Überprüfung jetzt in Ordnung ist.
Iirekm

8
Wie sieht die aktuelle Implementierung ab Scala 2.10 aus? Könnte jemand bitte einen Hinweis geben, wie viel Overhead dies in der Praxis bedeutet, und eine Faustregel, wann zu verwenden, wann zu vermeiden?
ib84

39

Es sieht so aus, als würde der Compiler ein Bitmap-Int-Feld auf Klassenebene einrichten, um mehrere Lazy-Felder als initialisiert (oder nicht) zu kennzeichnen, und das Zielfeld in einem synchronisierten Block initialisieren, wenn das relevante xor der Bitmap angibt, dass dies erforderlich ist.

Verwenden von:

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

erzeugt Beispielbytecode:

 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

In Tupeln wie lazy val (x,y) = { ... }initialisierte Werte haben das verschachtelte Caching über denselben Mechanismus. Das Tupelergebnis wird träge ausgewertet und zwischengespeichert, und ein Zugriff von entweder x oder y löst die Tupelauswertung aus. Das Extrahieren des einzelnen Werts aus dem Tupel erfolgt unabhängig und träge (und zwischengespeichert). So dass der obige doppel Instanziierung Code generiert ein x, yund ein x$1Feld vom Typ Tuple2.


25

Mit Scala 2.10 ein fauler Wert wie:

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

wird zu Bytecode kompiliert, der dem folgenden Java-Code ähnelt:

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

Beachten Sie, dass die Bitmap durch a dargestellt wird boolean. Wenn Sie ein weiteres Feld hinzufügen, vergrößert der Compiler das Feld so, dass mindestens 2 Werte dargestellt werden können, z byte. Dies gilt nur für große Klassen.

Aber Sie fragen sich vielleicht, warum das funktioniert? Die threadlokalen Caches müssen beim Eingeben eines synchronisierten Blocks gelöscht werden, damit der nichtflüchtige xWert in den Speicher geschrieben wird. Dieser Blog-Artikel gibt eine Erklärung .


11

Scala SIP-20 schlägt eine neue Implementierung von Lazy Val vor, die korrekter, aber ~ 25% langsamer als die "aktuelle" Version ist.

Die vorgeschlagene Implementierung sieht folgendermaßen aus:

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

Bis Juni 2013 wurde dieses SIP nicht genehmigt. Ich gehe davon aus, dass es wahrscheinlich genehmigt und in eine zukünftige Version von Scala aufgenommen wird, basierend auf der Mailinglistendiskussion. Ich denke, Sie sollten die Beobachtung von Daniel Spiewak beachten :

Lazy Val ist * nicht * kostenlos (oder sogar billig). Verwenden Sie es nur, wenn Sie unbedingt Faulheit für die Richtigkeit benötigen, nicht für die Optimierung.


10

Ich habe einen Beitrag zu diesem Thema geschrieben: https://dzone.com/articles/cost-laziness

Kurz gesagt, die Strafe ist so gering, dass Sie sie in der Praxis ignorieren können.


1
Danke für diesen Benchmark. Können Sie auch einen Vergleich mit den von SIP-20 vorgeschlagenen Implementierungen anstellen?
Turadg

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.