Une fonctionnalité pratique de Scala est lazy val
, où l'évaluation d'un val
est retardée jusqu'à ce qu'elle soit nécessaire (au premier accès).
Bien sûr, un lazy val
doit avoir une surcharge - quelque part Scala doit garder une trace de si la valeur a déjà été évaluée et l'évaluation doit être synchronisée, car plusieurs threads peuvent essayer d'accéder à la valeur pour la première fois à le même temps.
Quel est exactement le coût d'un lazy val
- existe-t-il un drapeau booléen caché associé à un lazy val
pour savoir s'il a été évalué ou non, qu'est-ce qui est exactement synchronisé et y a-t-il d'autres coûts?
De plus, supposons que je fasse ceci:
class Something {
lazy val (x, y) = { ... }
}
Est-ce la même chose que d'avoir deux lazy val
s x
et y
ou est-ce que je ne reçois la surcharge qu'une seule fois, pour la paire (x, y)
?
Ceci est tiré de la liste de diffusion scala et donne les détails d'implémentation de lazy
en termes de Java code (plutôt que bytecode):
class LazyTest {
lazy val msg = "Lazy"
}
est compilé en quelque chose d'équivalent au code Java 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;
}
}
Il semble que le compilateur organise un champ int bitmap de niveau classe pour marquer plusieurs champs paresseux comme initialisés (ou non) et initialise le champ cible dans un bloc synchronisé si le xor pertinent du bitmap indique qu'il est nécessaire.
En utilisant:
class Something {
lazy val foo = getFoo
def getFoo = "foo!"
}
produit un exemple de bytecode:
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
Les valeurs initialisées en tuples comme lazy val (x,y) = { ... }
ont imbriqué la mise en cache via le même mécanisme. Le résultat de Tuple est évalué et mis en cache paresseusement, et un accès de x ou y déclenchera l'évaluation de Tuple. L'extraction de la valeur individuelle du Tuple se fait de manière indépendante et paresseuse (et mise en cache). Ainsi, le code de double instanciation ci-dessus génère un x
, y
et un champ x$1
De type Tuple2
.
Avec Scala 2.10, une valeur paresseuse comme:
class Example {
lazy val x = "Value";
}
est compilé en code octet qui ressemble au code Java 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;
}
}
}
Notez que le bitmap est représenté par un boolean
. Si vous ajoutez un autre champ, le compilateur augmentera la taille du champ pour pouvoir représenter au moins 2 valeurs, c'est-à-dire sous la forme d'un byte
. Cela continue juste pour les classes énormes.
Mais vous vous demandez peut-être pourquoi cela fonctionne? Les caches de thread local doivent être effacés lors de la saisie d'un bloc synchronisé de telle sorte que la valeur non volatile x
soit vidée en mémoire. Cet article de blog donne ne explication .
Scala SIP-2 propose une nouvelle implémentation de lazy val, qui est plus correcte mais ~ 25% plus lente que la version "actuelle".
implémentation proposée ressemble à:
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
}
}
En juin 2013, ce SIP n'a pas été approuvé. Je m'attends à ce qu'il soit probablement approuvé et inclus dans une future version de Scala basé sur le mailing Par conséquent, je pense que vous seriez sage de tenir compte observation de Daniel Spiewak :
Lazy val n'est * pas * gratuit (ou même pas cher). Utilisez-le uniquement si vous avez absolument besoin de paresse pour l'exactitude, pas pour l'optimisation.
J'ai écrit un article concernant ce problème https://dzone.com/articles/cost-laziness
En résumé, la pénalité est si faible qu'en pratique, vous pouvez l'ignorer.