¿Cuál es el costo (oculto) del vago val de Scala?

165

Una característica útil de Scala es lazy val, donde la evaluación de un valse retrasa hasta que sea necesario (en el primer acceso).

Por supuesto, un lazy valdebe tener algunos gastos generales: en algún lugar, Scala debe realizar un seguimiento de si el valor ya se ha evaluado y si la evaluación debe estar sincronizada, ya que varios subprocesos pueden intentar acceder al valor por primera vez al mismo tiempo.

¿Cuál es exactamente el costo de un lazy val? ¿Hay un indicador booleano oculto asociado con un lazy valseguimiento si se ha evaluado o no, qué se sincroniza exactamente y hay más costos?

Además, supongamos que hago esto:

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

Es esto lo mismo que tener dos separadas lazy vals xy yo qué tengo la cabeza una sola vez, para el par (x, y)?

Jesper
fuente

Respuestas:

86

Esto se toma de la lista de correo scala y proporciona detalles de implementación lazyen términos de código Java (en lugar de bytecode):

class LazyTest {
  lazy val msg = "Lazy"
}

se compila en algo equivalente al siguiente código 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;
  }

}
oxbow_lakes
fuente
33
Creo que la implementación debe haber cambiado desde que se publicó esta versión de Java en 2007. Solo hay un bloque sincronizado y el bitmap$0campo es volátil en la implementación actual (2.8).
Mitch Blevins
1
Sí, ¡debería haber prestado más atención a lo que estaba publicando!
oxbow_lakes
8
@Mitch - ¡ Espero que la implementación haya cambiado! El anti-patrón de inicialización doblemente verificado es un error sutil clásico. Ver en.wikipedia.org/wiki/Double-checked_locking
Malvolio
20
Era antipatrón hasta Java 1.4. Dado que la palabra clave volátil Java 1.5 tiene un significado un poco más estricto y ahora esta doble verificación está bien.
iirekm
8
Entonces, a partir de scala 2.10, ¿cuál es la implementación actual? Además, ¿podría alguien dar una pista sobre la cantidad de gastos generales que esto significa en la práctica y alguna regla general cuando usar, cuándo evitar?
ib84
39

Parece que el compilador organiza un campo int de mapa de bits de nivel de clase para marcar múltiples campos perezosos como inicializados (o no) e inicializa el campo de destino en un bloque sincronizado si el xor relevante del mapa de bits indica que es necesario.

Utilizando:

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

produce un código de bytes de muestra:

 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

Los valores iniciados en tuplas como lazy val (x,y) = { ... }tienen almacenamiento en caché anidado a través del mismo mecanismo. El resultado de la tupla se evalúa y almacena en caché de forma perezosa, y un acceso de x o y activará la evaluación de la tupla. La extracción del valor individual de la tupla se realiza de forma independiente y perezosa (y en caché). Así que el código de doble ejemplificación anterior genera una x, yy un x$1campo de tipo Tuple2.

Mitch Blevins
fuente
26

Con Scala 2.10, un valor vago como:

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

se compila en código de bytes que se parece al siguiente código 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;
    }
  }
}

Tenga en cuenta que el mapa de bits está representado por a boolean. Si agrega otro campo, el compilador aumentará el tamaño del campo para poder representar al menos 2 valores, es decir, como a byte. Esto solo continúa para las grandes clases.

Pero te preguntarás por qué esto funciona. Las memorias caché locales de subprocesos deben borrarse al ingresar un bloque sincronizado de modo que el xvalor no volátil se vacíe en la memoria. Este artículo de blog da una explicación .

Rafael Winterhalter
fuente
11

Scala SIP-20 propone una nueva implementación de lazy val, que es más correcta pero ~ 25% más lenta que la versión "actual".

La implementación propuesta se ve así:

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 junio de 2013, este SIP no ha sido aprobado. Espero que sea aprobado e incluido en una versión futura de Scala basada en la discusión de la lista de correo. En consecuencia, creo que sería prudente prestar atención a la observación de Daniel Spiewak :

Lazy val es * no * gratis (o incluso barato). Úselo solo si realmente necesita pereza para la corrección, no para la optimización.

Leif Wickland
fuente
10

He escrito una publicación con respecto a este tema https://dzone.com/articles/cost-laziness

En pocas palabras, la penalización es tan pequeña que en la práctica puedes ignorarla.

romano
fuente
1
Gracias por ese punto de referencia. ¿También puede comparar con las implementaciones propuestas SIP-20?
Turadg
-6

dado el bycode generado por scala para perezoso, puede sufrir un problema de seguridad de hilo como se menciona en el bloqueo de doble verificación http://www.javaworld.com/javaworld/jw-05-2001/jw-0525-double.html?page=1

Huy Le
fuente
3
Este reclamo también fue hecho por un comentario a la respuesta aceptada por mitch y refutado por @iirekm: Este patrón está bien desde java1.5 en adelante.
Jens Schauder