Какова (скрытая) стоимость ленивого значения Scala?

Одной из удобных функций Scala является lazy val , где оценка val задерживается до тех пор, пока это не будет необходимо (при первом доступе).

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

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

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

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

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

Это взято из списка рассылки 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; } } 

Похоже, что компилятор упорядочивает поле битового массива на уровне classа для обозначения нескольких ленивых полей как инициализированных (или нет) и инициализирует целевое поле в синхронизированном блоке, если соответствующий 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) = { ... } имеют nested кэширование через один и тот же механизм. Результат кортежа лениво оценивается и кэшируется, и доступ либо x, либо y будет инициировать оценку кортежа. Извлечение индивидуального значения из кортежа выполняется независимо и лениво (и кэшируется). Таким образом, приведенный выше код двойной копии генерирует поле x , y и x$1 типа Tuple2 .

С 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 . Если вы добавите другое поле, компилятор увеличит размер поля, чтобы он мог представлять не менее двух значений, то есть в виде byte . Это просто продолжается для огромных classов.

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

Scala SIP-20 предлагает новую реализацию lazy val, которая правильнее, но на ~ 25% медленнее, чем «текущая» версия.

Предлагаемая реализация выглядит так:

 class LazyCellBase { // in a Java file - we need a public bitmap_0 public static AtomicIntegerFieldUpdater 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 (или даже дешево). Используйте его только в том случае, если вам абсолютно нужна лень для правильности, а не для оптимизации.

Я написал сообщение об этой проблеме https://dzone.com/articles/cost-laziness

В двух словах, штраф настолько мал, что на практике вы можете его игнорировать.

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

  • Что случилось с использованием ассоциативности компиляторами?
  • Почему Parallel.ForEach намного быстрее, чем AsParallel (). ForAll (), хотя MSDN предлагает иное?
  • Зачем закрывать class?
  • Производительность встроенных типов: char vs short vs int vs. float vs. double
  • Производительность Cellfun и Simple Matlab Loop
  • Каков самый быстрый способ слияния / объединения data.frames в R?
  • System.IO.FileSystemWatcher для отслеживания папки сетевого сервера - соображения производительности
  • В .NET, какой цикл работает быстрее, «for» или «foreach»?
  • Производительность HTTP против HTTPS
  • Android: RunOnUiThread против AsyncTask
  • Производительность MongoDB по агрегационным запросам
  • Давайте будем гением компьютера.