Quel est le coût (caché) du val paresseux de Scala?

165

Une fonctionnalité pratique de Scala est lazy valque l'évaluation d'un valest retardée jusqu'à ce qu'elle soit nécessaire (au premier accès).

Bien sûr, cela lazy valdoit avoir une surcharge - quelque part, Scala doit savoir 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 en même temps.

Quel est exactement le coût d'un lazy val- y a-t-il un indicateur booléen caché associé à a lazy valpour suivre 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 vals séparés xet you est-ce que je reçois la surcharge une seule fois, pour la paire (x, y)?

Jesper
la source

Réponses:

86

Ceci est tiré de la liste de diffusion scala et donne des détails d'implémentation lazyen termes de code Java (plutôt que de bytecode):

class LazyTest {
  lazy val msg = "Lazy"
}

est compilé en quelque chose d'équivalent au code Java suivant:

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
la source
33
Je pense que l'implémentation a dû changer depuis que cette version Java a été publiée en 2007. Il n'y a qu'un seul bloc synchronisé et le bitmap$0champ est volatile dans l'implémentation actuelle (2.8).
Mitch Blevins
1
Oui, j'aurais dû prêter plus d'attention à ce que je publiais!
oxbow_lakes
8
@Mitch - J'espère que l'implémentation a changé! L'anti-pattern d'initialisation vérifié deux fois est un bogue subtil classique. Voir en.wikipedia.org/wiki/Double-checked_locking
Malvolio
20
C'était anti-modèle jusqu'à Java 1.4. Étant donné que le mot-clé volatile Java 1.5 a une signification un peu plus stricte, une telle double vérification est maintenant acceptable.
iirekm
8
Donc, à partir de scala 2.10, quelle est l'implémentation actuelle? Aussi, pourrait-on s'il vous plaît quelqu'un donner un indice de combien de frais généraux cela signifie en pratique et une règle de base quand utiliser, quand éviter?
ib84
39

Il semble que le compilateur organise un champ int bitmap au niveau de la 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 échantillon 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 paraphées dans les tuples comme lazy val (x,y) = { ... }ont une mise en cache imbriquée via le même mécanisme. Le résultat du tuple est évalué et mis en cache paresseusement, et un accès de x ou de y déclenchera l'évaluation du tuple. L'extraction de la valeur individuelle du tuple est effectuée indépendamment et paresseusement (et mise en cache). Ainsi , le code ci - dessus double instanciation génère un x, yet un x$1champ de type Tuple2.

Mitch Blevins
la source
26

Avec Scala 2.10, une valeur paresseuse comme:

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

est compilé en code d'octet qui ressemble au code Java suivant:

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 forme de byte. Cela continue juste pour les classes énormes.

Mais vous vous demandez peut-être pourquoi cela fonctionne? Les caches locaux de thread doivent être effacés lors de la saisie d'un bloc synchronisé de sorte que la xvaleur non volatile soit vidée en mémoire. Cet article de blog donne une explication .

Rafael Winterhalter
la source
11

Scala SIP-20 propose une nouvelle implémentation de lazy val, qui est plus correcte mais ~ 25% plus lente que la version "actuelle".

L' implémentation proposée ressemble à ceci:

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 pense qu'il sera probablement approuvé et inclus dans une future version de Scala basée sur la discussion de la liste de diffusion. Par conséquent, je pense qu'il serait sage de tenir compte de l'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.

Leif Wickland
la source
10

J'ai écrit un article concernant ce problème https://dzone.com/articles/cost-laziness

En bref, la pénalité est si faible qu'en pratique, vous pouvez l'ignorer.

romain
la source
1
Merci pour cette référence. Pouvez-vous également comparer les implémentations proposées par SIP-20?
Turadg
-6

étant donné le bycode généré par scala pour lazy, il peut souffrir d'un problème de sécurité des threads comme mentionné dans le verrouillage de double vérification http://www.javaworld.com/javaworld/jw-05-2001/jw-0525-double.html?page=1

Huy Le
la source
3
Cette affirmation a également été faite par un commentaire à la réponse acceptée par mitch et réfutée par @iirekm: Ce modèle est correct à partir de java1.5.
Jens Schauder