Qual è il costo (nascosto) del pigro val di Scala?


165

Una caratteristica utile di Scala è lazy val, in cui la valutazione di a valè ritardata fino a quando non è necessaria (al primo accesso).

Naturalmente, è lazy valnecessario avere un certo sovraccarico: da qualche parte Scala deve tenere traccia del fatto che il valore sia già stato valutato e la valutazione debba essere sincronizzata, poiché più thread potrebbero tentare di accedere al valore per la prima volta contemporaneamente.

Qual è esattamente il costo di a lazy val- c'è un flag booleano nascosto associato a a lazy valper tenere traccia se è stato valutato o no, che cosa è esattamente sincronizzato e ci sono altri costi?

Inoltre, supponiamo che io faccia questo:

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

È questo lo stesso che avere due distinti lazy vals xe yo faccio ad avere il sovraccarico solo una volta, per la coppia (x, y)?

Risposte:


86

Questo è preso dalla mailing list di scala e fornisce dettagli di implementazione lazyin termini di codice Java (piuttosto che bytecode):

class LazyTest {
  lazy val msg = "Lazy"
}

viene compilato in modo equivalente al seguente codice 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
Penso che l'implementazione debba essere cambiata da quando questa versione di Java è stata pubblicata nel 2007. C'è solo un blocco sincronizzato e il bitmap$0campo è volatile nell'attuale implementazione (2.8).
Mitch Blevins,

1
Sì, avrei dovuto prestare maggiore attenzione a ciò che stavo postando!
oxbow_lakes,

8
@Mitch - Spero che l'implementazione sia cambiata! L'anti-pattern di inizializzazione ricontrollato è un classico bug sottile. Vedi en.wikipedia.org/wiki/Double-checked_locking
Malvolio il

20
Era antipattern fino a Java 1.4. Poiché la parola chiave volatile Java 1.5 ha un significato un po 'più rigoroso e ora tale doppio controllo è OK.
iirekm,

8
Quindi, a partire da scala 2.10, qual è l'implementazione attuale? Inoltre, per favore qualcuno potrebbe dare un indizio di quanto overhead questo significa in pratica e qualche regola empirica quando usare, quando evitare?
ib84

39

Sembra che il compilatore organizzi un campo int bitmap a livello di classe per contrassegnare più campi pigri come inizializzati (o meno) e inizializza il campo target in un blocco sincronizzato se il relativo xor della bitmap indica che è necessario.

usando:

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

produce un bytecode di esempio:

 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

I valori inizializzati in tuple come lazy val (x,y) = { ... }hanno la cache nidificata tramite lo stesso meccanismo. Il risultato della tupla viene pigramente valutato e memorizzato nella cache e un accesso di x o y attiverà la valutazione della tupla. L'estrazione del valore individuale dalla tupla viene eseguita in modo indipendente e pigramente (e memorizzato nella cache). Quindi il codice doppio esemplificazione sopra genera un x, ye un x$1campo di tipo Tuple2.


26

Con Scala 2.10, un valore pigro come:

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

viene compilato in codice byte simile al seguente codice 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;
    }
  }
}

Si noti che la bitmap è rappresentata da a boolean. Se aggiungi un altro campo, il compilatore aumenterà la dimensione del campo per poter rappresentare almeno 2 valori, ovvero come a byte. Questo vale solo per le lezioni enormi.

Ma potresti chiederti perché funziona? Le cache thread-local devono essere cancellate quando si inserisce un blocco sincronizzato in modo tale che il xvalore non volatile venga scaricato nella memoria. Questo articolo di blog fornisce una spiegazione .


11

Scala SIP-20 propone una nuova implementazione di Lazy Val, che è più corretta ma ~ 25% più lenta della versione "attuale".

L' implementazione proposta è simile a:

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

A partire da giugno 2013 questo SIP non è stato approvato. Mi aspetto che sia probabile che venga approvato e incluso in una versione futura di Scala sulla base della discussione sulla mailing list. Di conseguenza, penso che saresti saggio dare ascolto all'osservazione di Daniel Spiewak :

Lazy val non è * non * gratuito (o anche economico). Usalo solo se hai assolutamente bisogno di pigrizia per correttezza, non per ottimizzazione.



Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.