161 votes

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

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 certaine surcharge - Scala doit savoir si la valeur a déjà été évaluée et l'évaluation doit être synchronisée, car plusieurs threads pourraient essayer d'accéder à la valeur pour la première fois en même temps.

Quel est le coût exact d'un lazy val - existe-t-il un drapeau booléen caché associé à une lazy val pour savoir si elle a été évaluée ou non, ce qui est exactement synchronisé et s'il y a des coûts supplémentaires ?

En outre, supposons que je fasse ça :

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

Est-ce que c'est la même chose que d'avoir deux lazy val s x y y ou est-ce que je reçois les frais généraux une seule fois, pour la paire (x, y) ?

82voto

oxbow_lakes Points 70013

Ceci est tiré du liste de diffusion scala et donne les détails de mise en œuvre de lazy en 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;
  }

}

38voto

Mitch Blevins Points 7646

Il semble que le compilateur s'arrange pour qu'un champ int bitmap de niveau classe signale plusieurs champs paresseux comme initialisés (ou non) et initialise le champ cible dans un bloc synchronisé si le xor pertinent du bitmap indique que c'est nécessaire.

Utilisation :

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 dans des 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é paresseusement et mis en cache, et un accès à x ou 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 de double-instanciation ci-dessus génère une valeur de x , y et un x$1 champ de type Tuple2 .

25voto

raphw Points 6008

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 l'image bitmap est représentée par un fichier boolean . Si vous ajoutez un autre champ, le compilateur augmentera la taille du champ pour qu'il puisse représenter au moins 2 valeurs, c'est à dire en tant que byte . Cela continue pour des classes énormes.

Mais vous vous demandez peut-être pourquoi cela fonctionne ? Les caches locaux des threads doivent être vidés lors de l'entrée dans un bloc synchronisé, de sorte que les caches non volatils de l'unité d'exécution soient vidés. x est vidée en mémoire. Cet article de blog donne une explication .

11voto

Leif Wickland Points 1954

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

El proposition de mise en œuvre 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 en fonction de la discussion sur la liste de diffusion. Par conséquent, je pense qu'il serait sage de tenir compte de ce qui suit L'observation de Daniel Spiewak :

Lazy val n'est *pas* gratuit (ni même bon marché). Ne l'utilisez que si vous avez absolument absolument besoin de la paresse pour la correction, et non pour l'optimisation.

-6voto

Huy Le Points 181

Étant donné le bycode généré par scala pour lazy, il peut souffrir d'un problème de thread safety comme mentionné dans double check locking http://www.javaworld.com/javaworld/jw-05-2001/jw-0525-double.html?page=1

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X