tutorial learn koans exercises codecademy performance scala lazy-evaluation

performance - learn - ¿Cuál es el costo(oculto) del val perezoso de Scala?



scala learn (6)

Con Scala 2.10, un valor lento como:

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

se compila en código de bytes que se asemeja al siguiente código de 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 un boolean . Si agrega otro campo, el compilador aumentará el tamaño del campo para poder representar al menos 2 valores, es decir, como un byte . Esto solo sigue para grandes clases.

Pero podrías preguntarte por qué funciona esto? Las memorias caché locales de subprocesos deben borrarse al ingresar a un bloque sincronizado de modo que el valor de x no volátil se vacíe en la memoria. Este artículo de blog da una explicación .

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

Por supuesto, un valor lazy val debe tener cierta sobrecarga, en algún lugar Scala debe realizar un seguimiento de si el valor ya se ha evaluado y 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 valor lazy val ? ¿Hay un indicador booleano oculto asociado con un valor lazy val para realizar un seguimiento si se ha evaluado o no, qué está exactamente sincronizado y hay más costos?

Además, supongamos que hago esto:

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

¿Es esto lo mismo que tener dos lazy val separados x y solo obtengo la sobrecarga una vez para el par (x, y) ?


Esto se toma de la lista de correo de scala y proporciona detalles de implementación de lazy en 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 de 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; } }



Parece que el compilador organiza un campo int de mapa de bits de nivel de clase para marcar varios campos diferidos como inicializados (o no) e inicializa el campo objetivo en un bloque sincronizado si el xor correspondiente del mapa de bits indica que es necesario.

Utilizando:

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

produce un bytecode 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 con iniciales en tuplas como lazy val (x,y) = { ... } han anidado el almacenamiento en caché mediante el mismo mecanismo. El resultado de la tupla se evalúa y almacena en caché, 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é). Por lo tanto, el código de doble instancia anterior genera un campo x , y , y un x$1 de tipo Tuple2 .



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 partir de 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 harías bien en prestar atención a la observación de Daniel Spiewak :

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