Какая (скрытая) стоимость Scala lazy val?
Одна удобная функция Scala - lazy val
, где оценка a val
задерживается до тех пор, пока она не понадобится (при первом доступе).
Конечно, a lazy val
должен иметь некоторые накладные расходы - где-то Scala должен отслеживать, было ли значение уже оценено, и оценка должна быть синхронизирована, поскольку несколько потоков могут попытаться получить доступ к значению в первый раз в то же время.
В чем именно заключается стоимость lazy val
- существует ли скрытый логический флаг, связанный с lazy val
, чтобы отслеживать, если он был оценен или нет, что именно синхронизировано и есть ли какие-либо затраты?
Кроме того, предположим, что я делаю это:
class Something {
lazy val (x, y) = { ... }
}
Это то же самое, что два отдельных lazy val
x
и y
, или я получаю служебные данные только один раз, для пары (x, y)
?
Ответы
Ответ 1
Это взято из 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;
}
}
Ответ 2
Похоже, что компилятор упорядочивает для поля класса bitmap 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
.
Ответ 3
С 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
будет сброшено в память. В этой статье в блоге объяснение.
Ответ 4
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 на основе обсуждения списка рассылки. Следовательно, я думаю, вам было бы разумно прислушаться наблюдение Даниэля Спивака:
Lazy val is * not * free (или даже дешево). Используйте его, только если вы абсолютно нужна лень для правильности, а не для оптимизации.
Ответ 5
Я написал сообщение об этом вопросе https://dzone.com/articles/cost-laziness
Вкратце, штраф настолько мал, что на практике вы можете его игнорировать.
Ответ 6
с учетом байтового кода, генерируемого scala для лени, он может страдать от проблемы безопасности потока, как указано в двойной блокировке проверки http://www.javaworld.com/javaworld/jw-05-2001/jw-0525-double.html?page=1