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;
}
}
He escrito una publicación con respecto a este tema https://dzone.com/articles/cost-laziness
En pocas palabras, la pena es tan pequeña que en la práctica puedes ignorarla.
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
.
dado el bycode generado por scala for flojo, puede sufrir problemas de seguridad de hilo como se menciona en el bloqueo de doble bloqueo http://www.javaworld.com/javaworld/jw-05-2001/jw-0525-double.html?page=1
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.