development

Scala 게으른 발의 (숨겨진) 비용은 얼마입니까?

big-blog 2020. 6. 3. 08:05
반응형

Scala 게으른 발의 (숨겨진) 비용은 얼마입니까?


Scala의 편리한 기능 중 하나 lazy valval입니다 (필수에 액세스 할 때까지).

물론, lazy val약간의 오버 헤드 있어야합니다. 어딘가에 Scala는 값이 이미 평가되었는지와 평가를 동기화해야하는지 추적해야합니다. 여러 스레드가 동시에 처음으로 값에 액세스하려고 시도 할 수 있기 때문입니다.

정확히 비용은 얼마입니까-평가 여부에 따라 추적을 유지하기 위해 lazy val숨겨진 부울 플래그 lazy val가 있습니까? 정확히 동기화되고 더 많은 비용이 있습니까?

또한 내가 이것을한다고 가정 해보십시오.

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

이 두 개의 분리 된 것으로 동일 lazy valxy한 쌍을 위해, 또는 내가 한 번만 오버 헤드를받을 수 있나요 (x, y)?


이것은 scala 메일 링리스트 에서 가져 왔으며 lazy바이트 코드가 아닌 Java 코드로 구현 세부 사항을 제공합니다 .

class LazyTest {
  lazy val msg = "Lazy"
}

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

}

컴파일러는 클래스 수준의 비트 맵 int 필드가 여러 개의 지연 필드를 초기화 된 것으로 플래그 지정하도록 정렬하고 비트 맵의 ​​관련 xor에 필요한 경우 동기화 된 블록에서 대상 필드를 초기화하는 것처럼 보입니다.

사용 :

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

샘플 바이트 코드를 생성합니다.

 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

Values initialed in tuples like lazy val (x,y) = { ... } have nested caching via the same mechanism. The tuple result is lazily evaluated and cached, and an access of either x or y will trigger the tuple evaluation. Extraction of the individual value from the tuple is done independently and lazily (and cached). So the above double-instantiation code generates an x, y, and an x$1 field of type Tuple2.


With Scala 2.10, a lazy value like:

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

is compiled to byte code that resembles the following Java code:

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

Note that the bitmap is represented by a boolean. If you add another field, the compiler will increase the size of the field to being able to represent at least 2 values, i.e. as a byte. This just goes on for huge classes.

But you might wonder why this works? The thread-local caches must be cleared when entering a synchronized block such that the non-volatile x value is flushed into memory. This blog article gives an explanation.


Scala SIP-20 proposes a new implementation of lazy val, which is more correct but ~25% slower than the "current" version.

The proposed implementation looks like:

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

As of June 2013 this SIP hasn't been approved. I expect that it's likely to be approved and included in a future version of Scala based on the mailing list discussion. Consequently, I think you'd be wise to heed Daniel Spiewak's observation:

Lazy val is *not* free (or even cheap). Use it only if you absolutely need laziness for correctness, not for optimization.


I've written a post with regard to this issue https://dzone.com/articles/cost-laziness

In nutshell, the penalty is so small that in practice you can ignore it.


given the bycode generated by scala for lazy, it can suffer thread safety problem as mentioned in double check locking http://www.javaworld.com/javaworld/jw-05-2001/jw-0525-double.html?page=1

참고URL : https://stackoverflow.com/questions/3041253/whats-the-hidden-cost-of-scalas-lazy-val

반응형