Яка (прихована) вартість лінивого валу Скали?


165

Одна зручна особливість Scala полягає в тому lazy val, коли оцінка valзатримки затримується до необхідності (при першому доступі).

Звичайно, lazy valобов'язково мають бути деякі накладні витрати - десь Scala повинен відслідковувати, чи значення вже було оцінено, і оцінку потрібно синхронізувати, тому що кілька потоків можуть намагатися отримати доступ до цього значення одночасно.

Яка саме вартість lazy val- чи є прихований булевий прапор, пов’язаний з a lazy valдля відстеження, якщо він був оцінений чи ні, що саме синхронізовано і чи є ще витрати?

Крім того, припустимо, що я роблю це:

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

Це те ж саме, що мати два окремих lazy vals xі yчи я отримую накладні витрати лише один раз для пари (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).
Мітч Блевінс

1
Так - я мав би приділити більше уваги тому, що я розміщував!
oxbow_lakes

8
@Mitch - Я сподіваюся, що реалізація змінилася! Подвійно перевірений антидіапазон ініціалізації - це класична непомітна помилка. Дивіться en.wikipedia.org/wiki/Double-cont_locking
Мальволіо,

20
Це було антипатерном аж до Java 1.4. Оскільки Java 1.5 мінливе ключове слово має трохи суворіше значення, і тепер така подвійна перевірка в порядку.
iirekm

8
Отже, що стосується шкали 2.10, яка зараз реалізація? Також, будь ласка, хтось підкаже, підкажіть, скільки накладних витрат це означає на практиці та якесь правило, коли користуватися, коли уникати?
ib84

39

Схоже, компілятор організовує для рівня поля растрових зображень класу позначення декількох ледачих полів як ініціалізованих (чи ні) та ініціалізує цільове поле у ​​синхронізованому блоці, якщо відповідний 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, yі в x$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;
    }
  }
}

Зауважте, що растровий малюнок представлений символом a boolean. Якщо ви додасте інше поле, компілятор збільшить розмір поля, щоб він міг представляти щонайменше 2 значення, тобто як a byte. Це просто продовжується для величезних занять.

Але вам може бути цікаво, чому це працює? Локальні кешові потоки повинні бути очищені при введенні синхронізованого блоку таким чином, щоб енергонезалежне xзначення передавалося в пам'ять. Ця стаття в блозі дає пояснення .


11

Scala SIP-20 пропонує нову реалізацію ледачого val, що є більш правильним, але на 25% повільніше, ніж "поточна" версія.

У запропонованій реалізації виглядає наступним чином :

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 року цей SIP не затверджено. Я очікую, що це, ймовірно, буде затверджено та включено до майбутньої версії Scala на основі обговорення списку розсилки. Отже, я думаю, вам було б розумно прислухатися до спостереження Даніеля Шпієка :

Ледачий вал * не * безкоштовно (або навіть дешево). Використовуйте його, лише якщо вам абсолютно потрібна лінь для коректності, а не для оптимізації.


10

Я написав пост стосовно цього питання https://dzone.com/articles/cost-laziness

Коротше кажучи, штраф настільки малий, що на практиці його можна ігнорувати.


1
Дякую за цей тест. Чи можете ви також орієнтуватися на запропоновані реалізації SIP-20?
Турадг

-6

зважаючи на те, що байкод, сформований шкалою для ледачих, він може зазнати проблеми безпеки потоку, як це було зазначено у подвійному блокуванні перевірки http://www.javaworld.com/javaworld/jw-05-2001/jw-0525-double.html?page=1


3
Цю заяву також зробив коментар до прийнятої відповіді Мітчем та спростував @iirekm: Ця картина чудова від java1.5 і далі.
Єнс Шодер
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.