Qual é o custo (oculto) do val preguiçoso de Scala?

165

Um recurso útil do Scala é lazy valque a avaliação de a valé adiada até que seja necessária (no primeiro acesso).

Obviamente, é lazy valnecessário ter alguma sobrecarga - em algum lugar o Scala deve acompanhar se o valor já foi avaliado e a avaliação deve ser sincronizada, porque vários encadeamentos podem tentar acessar o valor pela primeira vez ao mesmo tempo.

Qual é exatamente o custo de um lazy val- existe um sinalizador booleano oculto associado a um lazy valpara acompanhar se foi avaliado ou não, o que exatamente está sincronizado e há mais custos?

Além disso, suponha que eu faça isso:

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

É o mesmo que ter dois lazy vals separados xe you recebo a sobrecarga apenas uma vez para o par (x, y)?

Jesper
fonte

Respostas:

86

Isso é retirado da lista de discussão scala e fornece detalhes de implementação lazyem termos de código Java (em vez de código de bytes):

class LazyTest {
  lazy val msg = "Lazy"
}

é compilado para algo equivalente ao seguinte código 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;
  }

}
oxbow_lakes
fonte
33
Eu acho que a implementação deve ter mudado desde que esta versão do Java foi lançada em 2007. Existe apenas um bloco sincronizado e o bitmap$0campo é volátil na implementação atual (2.8).
Mitch Blevins
1
Sim - eu deveria ter prestado mais atenção ao que estava postando!
oxbow_lakes
8
@ Mitch - Espero que a implementação tenha mudado! O anti-padrão de inicialização verificado duas vezes é um bug sutil clássico. Veja en.wikipedia.org/wiki/Double-checked_locking
Malvolio
20
Era antipadrão até o Java 1.4. Como a palavra-chave volátil do Java 1.5 tem um significado um pouco mais rigoroso e agora essa verificação dupla está OK.
iirekm 28/09
8
Então, a partir do scala 2.10, qual é a implementação atual? Além disso, alguém poderia dar uma dica de quanto isso sobrecarga significa na prática e alguma regra prática quando usar, quando evitar?
Ib84
39

Parece que o compilador organiza um campo int de bitmap em nível de classe para sinalizar vários campos preguiçosos como inicializados (ou não) e inicializa o campo de destino em um bloco sincronizado se o xor relevante do bitmap indicar que é necessário.

Usando:

class Something {
  lazy val foo = getFoo
  def getFoo = "foo!"
}

produz bytecode de amostra:

 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

Valores inicializados em tuplas, como o lazy val (x,y) = { ... }cache aninhado pelo mesmo mecanismo. O resultado da tupla é avaliado e armazenado em cache preguiçosamente, e um acesso de x ou y acionará a avaliação da tupla. A extração do valor individual da tupla é feita de forma independente e lenta (e armazenada em cache). Assim, o código double-instanciação acima gera um x, ye um x$1campo de tipo Tuple2.

Mitch Blevins
fonte
26

Com o Scala 2.10, um valor lento como:

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

é compilado para código de bytes que se assemelha ao seguinte código 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;
    }
  }
}

Observe que o bitmap é representado por um boolean. Se você adicionar outro campo, o compilador aumentará o tamanho do campo para poder representar pelo menos 2 valores, como a byte. Isso acontece em grandes classes.

Mas você pode se perguntar por que isso funciona? Os caches locais do encadeamento devem ser limpos ao inserir um bloco sincronizado, para que o xvalor não volátil seja liberado na memória. Este artigo do blog fornece uma explicação .

Rafael Winterhalter
fonte
11

O Scala SIP-20 propõe uma nova implementação de lazy val, que é mais correta, mas ~ 25% mais lenta que a versão "atual".

A implementação proposta se parece com:

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

Em junho de 2013, este SIP não foi aprovado. Espero que seja aprovado e incluído em uma versão futura do Scala com base na discussão da lista de discussão. Consequentemente, acho que seria sensato atender à observação de Daniel Spiewak :

Val preguiçoso * não * é gratuito (ou até barato). Use-o somente se você precisar de preguiça para correção, não para otimização.

Leif Wickland
fonte
10

Escrevi um post sobre esse problema https://dzone.com/articles/cost-laziness

Em poucas palavras, a penalidade é tão pequena que, na prática, você pode ignorá-la.

romano
fonte
1
Obrigado por essa referência. Você também pode comparar as implementações propostas pelo SIP-20?
Turadg 29/01
-6

dado o bycode gerado pelo scala for preguiçoso, ele pode sofrer um problema de segurança de thread, conforme mencionado no bloqueio de verificação dupla http://www.javaworld.com/javaworld/jw-05-2001/jw-0525-double.html?page=1

Huy Le
fonte
3
Essa alegação também foi feita por um comentário à resposta aceita por mitch e refutada por @iirekm: Esse padrão é bom a partir do java1.5 em diante.
Jens Schauder