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


165

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

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

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

또한 내가 이렇게한다고 가정 해보십시오.

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

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

답변:


86

이것은 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;
  }

}

33
이 Java 버전이 2007 년에 게시 된 이후로 구현이 변경되어야한다고 생각합니다. 동기화 된 블록은 하나 뿐이며 bitmap$0현재 구현 (2.8) 에서는 필드가 일시적입니다.
Mitch Blevins 2018 년

1
예-게시 한 내용에 더주의를 기울여야했습니다!
oxbow_lakes

8
@Mitch- 구현이 변경 되었기를 바랍니다 ! 이중 검사 초기화 안티 패턴은 고전적인 미묘한 버그입니다. en.wikipedia.org/wiki/Double-checked_locking
Malvolio를

20
Java 1.4까지의 안티 패턴입니다. Java 1.5 휘발성 키워드는 약간 더 엄격한 의미를 가지므로 이제 이러한 이중 확인이 가능합니다.
iirekm

8
스칼라 2.10부터 현재 구현은 무엇입니까? 또한 누군가가 이것이 실제로 의미하는 오버 헤드의 양과 사용시, 피해야 할 때의 경험에 대한 힌트를 줄 수 있습니까?
ib84

39

컴파일러는 클래스 수준의 비트 맵 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

튜플로 초기화 된 값 lazy val (x,y) = { ... }은 동일한 메커니즘을 통해 중첩 된 캐싱 을가 집니다. 튜플 결과는 느리게 평가되고 캐시되며 x 또는 y에 액세스하면 튜플 평가가 트리거됩니다. 튜플에서 개별 값의 추출은 독립적으로 느리게 (및 캐시) 수행됩니다. 위의 두 인스턴스 코드는 생성 그래서 x, yx$1유형의 필드 Tuple2.


26

Scala 2.10에서 게으른 값은 다음과 같습니다.

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

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

비트 맵은로 표시됩니다 boolean. 다른 필드를 추가하면 컴파일러는 필드의 크기를 증가시켜 최소 2 개의 값을 나타낼 수 있습니다 (예 : a) byte. 이것은 거대한 수업에 계속 진행됩니다.

그러나 왜 이것이 작동하는지 궁금 할 것입니다. 비 휘발성 x값이 메모리로 플러시되도록 동기화 된 블록을 입력 할 때 스레드 로컬 캐시를 지워야합니다 . 이 블로그 기사는 설명을 제공 합니다 .


11

Scala SIP-20 은 "현재"버전보다 더 정확하지만 ~ 25 % 더 느린 lazy val의 새로운 구현을 제안합니다.

제안 구현 외모가 좋아 :

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

2013 년 6 월 현재이 SIP는 승인되지 않았습니다. 메일 링리스트 토론을 바탕으로 향후 버전의 Scala에 승인되어 포함될 것으로 예상됩니다. 결과적으로 Daniel Spiewak의 관찰 에 귀를 기울이는 것이 현명하다고 생각합니다 .

게으른 발은 무료가 아니거나 저렴하다. 최적화가 아닌 정확성을 위해 게으름이 절대적으로 필요한 경우에만 사용하십시오.


10

이 문제와 관련하여 게시물을 작성했습니다 https://dzone.com/articles/cost-laziness

간단히 말해, 패널티는 너무 작아서 실제로 무시할 수 있습니다.


1
벤치마킹 해 주셔서 감사합니다. SIP-20 제안 구현에 대해 벤치마킹 할 수 있습니까?
Turadg

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.