Какова (скрытая) стоимость ленивого вала Scala?
одной из удобных функций Scala является lazy val
, где оценки val
задерживается до тех пор, пока это не будет необходимо (при первом доступе).
конечно, a lazy val
должен иметь некоторые накладные расходы-где-то Scala должен отслеживать, было ли значение уже оценено, и оценка должна быть синхронизирована, потому что несколько потоков могут попытаться получить доступ к значению в первый раз одновременно.
какова именно стоимость a lazy val
- есть ли скрытые логический флаг, связанный с lazy val
отслеживать, если он был оценен или нет, что именно синхронизируется и еще какие-то расходы?
кроме того, предположим, что я делаю это:
class Something {
lazy val (x, y) = { ... }
}
это то же самое, что иметь два отдельных lazy val
s x
и y
или я получаю накладные расходы только один раз, для пары (x, y)
?
6 ответов:
Это взято из список рассылки scala и дает детали реализации
lazy
С точки зрения Java-кода (а не байт-кода):class LazyTest { lazy val msg = "Lazy" }
компилируется в нечто эквивалентное следующий Java код:
class LazyTest { public int bitmap; private String msg; public String msg() { if ((bitmap & 1) == 0) { synchronized (this) { if ((bitmap & 1) == 0) { synchronized (this) { msg = "Lazy"; } } bitmap = bitmap | 1; } } return msg; } }
похоже, что компилятор организует для поля класса bitmap int флаг нескольких ленивых полей как инициализированных (или нет) и инициализирует целевое поле в синхронизированном блоке, если соответствующий xor растрового изображения указывает, что это необходимо.
использование:
class Something { lazy val foo = getFoo def getFoo = "foo!" }
производит выборки байт-кода:
0 aload_0 [this] 1 getfield blevins.example.Something.bitmap : 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 : 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 : int [15] 37 iconst_1 38 ior 39 putfield blevins.example.Something.bitmap : 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
и
С Scala 2.10, ленивое значение, как:
class Example { lazy val x = "Value"; }
компилируется в байтовый код, который напоминает следующий код Java:
public class Example { private String x; private volatile boolean bitmap; public String x() { if(this.bitmap == true) { return this.x; } else { return x$lzycompute(); } } private String x$lzycompute() { synchronized(this) { if(this.bitmap != true) { this.x = "Value"; this.bitmap = true; } return this.x; } } }
обратите внимание, что растровое изображение представляет собой
boolean
. Если вы добавите другое поле, компилятор увеличит размер поля, чтобы иметь возможность представлять по крайней мере 2 значения, т. е. какbyte
. Это просто продолжается для огромных классов.но вы можете задаться вопросом, почему это работает? Локальные кэши потоков должны быть очищены, когда ввод синхронизированного блока такой, что энергонезависимый
x
значение записывается в память. Эта статья в блоге дает объяснение.
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 на основе обсуждения списка рассылки. Следовательно, я думаю, что вы были бы мудры, чтобы прислушаться Даниэль Спивак наблюдение:
ленивый вал *не* бесплатно (или даже дешево). Используйте его только если вы абсолютно нужна лень для корректности, а не для оптимизации.
Я написал сообщение по этому вопросу https://dzone.com/articles/cost-laziness
в двух словах, штраф настолько мал, что на практике вы можете его игнорировать.
учитывая байкод, сгенерированный scala для lazy, он может столкнуться с проблемой безопасности потока, как упоминалось в double check locking http://www.javaworld.com/javaworld/jw-05-2001/jw-0525-double.html?page=1