Какова (скрытая) стоимость ленивого Вэл Скала?

165

Одна удобная особенность Scala заключается в том lazy val, что оценка a valоткладывается до тех пор, пока это необходимо (при первом доступе).

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

Какова стоимость a lazy val- есть ли скрытый логический флаг, связанный с a, lazy valчтобы отслеживать, был ли он оценен или нет, что именно синхронизируется и есть ли какие-либо дополнительные затраты?

Кроме того, предположим, что я делаю это:

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

Это то же самое , как иметь два отдельных lazy valсек xи yили я получаю накладные расходы только один раз, для пары (x, y)?

Jesper
источник

Ответы:

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

}
oxbow_lakes
источник
33
Я думаю, что реализация, должно быть, изменилась, так как эта версия Java была опубликована в 2007 году. Существует только один синхронизированный блок, и bitmap$0поле в текущей реализации изменчиво (2.8).
Митч Блевинс
1
Да - я должен был уделить больше внимания тому, что я публиковал!
oxbow_lakes
8
@ Митч - я надеюсь, что реализация изменилась! Анти-паттерн инициализации с двойной проверкой является классической тонкой ошибкой. См. En.wikipedia.org/wiki/Double-checked_locking
Мальволио
20
Это был антипаттерн до Java 1.4. Начиная с Java 1.5 ключевое слово volatile имеет немного более строгий смысл, и теперь такая двойная проверка в порядке.
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, 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;
    }
  }
}

Обратите внимание, что растровое изображение представлено как boolean. Если вы добавите другое поле, компилятор увеличит размер поля, чтобы иметь возможность представлять как минимум 2 значения, то есть как byte. Это просто продолжается для огромных классов.

Но вы можете спросить, почему это работает? При входе в синхронизированный блок локальные кэши потоков должны быть очищены, чтобы энергонезависимое xзначение сбрасывалось в память. Эта статья в блоге дает объяснение .

Рафаэль Винтерхальтер
источник
11

Scala SIP-20 предлагает новую реализацию lazy 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

учитывая код, созданный scala для lazy, он может столкнуться с проблемой безопасности потоков, как упомянуто в двойной проверке блокировки http://www.javaworld.com/javaworld/jw-05-2001/jw-0525-double.html?page=1.

Хай Ле
источник
3
Это утверждение также было сделано в комментарии к принятому ответу mitch и опровергнуто @iirekm: этот шаблон работает с java1.5 и выше.
Йенс Шаудер