web-dev-qa-db-fra.com

Quel est le coût (caché) du val paresseux 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 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 vals x et y ou est-ce que je ne reçois la surcharge qu'une seule fois, pour la paire (x, y)?

160
Jesper

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;
  }

}
80
oxbow_lakes

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.

38
Mitch Blevins

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 .

24

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.

11
Leif Wickland

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.

9
Roman