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


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.