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)?

Jesper
quelle

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

}
oxbow_lakes
quelle
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.

Mitch Blevins
quelle
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 .

Rafael Winterhalter
quelle
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.

Leif Wickland
quelle
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.

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

Angesichts des von scala for faul generierten Bycodes kann es zu Problemen mit der Thread-Sicherheit kommen, wie unter Überprüfen der Sperre http://www.javaworld.com/javaworld/jw-05-2001/jw-0525-double.html?page=1 angegeben

Huy Le
quelle
3
Diese Behauptung wurde auch durch einen Kommentar zu der akzeptierten Antwort von Mitch gemacht und von @iirekm widerlegt: Dieses Muster ist ab Java1.5 in Ordnung.
Jens Schauder