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

Uma característica útil do Scala é o lazy val , onde a avaliação de um val é atrasada até que seja necessário (no primeiro access).

Naturalmente, um lazy val deve ter alguma sobrecarga – em algum lugar, o Scala deve controlar se o valor já foi avaliado e a avaliação deve ser sincronizada, porque vários segmentos podem tentar acessar o valor pela primeira vez ao mesmo tempo.

Qual é exatamente o custo de um lazy val – existe uma bandeira booleana oculta associada a um lazy val para rastrear se foi avaliada ou não, o que exatamente é sincronizado e há mais algum custo?

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

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

Isto é o mesmo que ter dois vals lazy val x e y lazy val separados ou eu recebo o overhead apenas uma vez, para o par (x, y) ?

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

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

Parece que o compilador organiza um campo int de bitmap em nível de class 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 iniciados em tuplas como lazy val (x,y) = { ... } possuem um cache nested através do mesmo mecanismo. O resultado da tupla é avaliado e armazenado em cache preguiçosamente, e um access de x ou y acionará a avaliação da tupla. A extração do valor individual da tupla é feita de forma independente e preguiçosa (e armazenada em cache). Portanto, o código de instanciação dupla acima gera um campo x , y e x$1 do tipo Tuple2 .

Com o Scala 2.10, um valor preguiçoso como:

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

é compilado para codificar byte 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 representar pelo menos dois valores, ou seja, como um byte . Isso só acontece em classs enormes.

Mas você pode se perguntar por que isso funciona? Os caches locais de thread devem ser limpos ao inserir um bloco sincronizado de forma que o valor x não volátil seja liberado na memory. Este artigo do blog dá uma explicação .

O Scala SIP-20 propõe uma nova implementação do preguiçoso val, que é mais correto, mas ~ 25% mais lento 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 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 provável 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 prestar atenção à observação de Daniel Spiewak :

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

Eu escrevi um post com relação a este problema https://dzone.com/articles/cost-laziness

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

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