Оптимизация компилятора Scala для неизменности

Оптимизирует ли компилятор scala использование памяти, удаляя ссылки на val, используемые только один раз в блоке?

Представьте себе объект, содержащий в совокупности некоторые огромные данные - достигнув размера, при котором клонирование данных или их производных вполне может поцарапать максимальный объем памяти для JVM/машины.

Минимальный пример кода, но представьте себе более длинную цепочку преобразований данных:

val huge: HugeObjectType
val derivative1 = huge.map(_.x)
val derivative2 = derivative1.groupBy(....)

Будет ли компилятор, например. оставить huge помеченным как пригодный для сборки мусора после того, как derivative1 будет вычислено? или он будет поддерживать его до тех пор, пока блок упаковки не будет закрыт?

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


person matanster    schedule 22.11.2015    source источник
comment
Я не думаю, что компилятор может многое сделать с GC (который является временем выполнения). Проверьте stackoverflow.com/questions/6275397/< /а>   -  person ale64bit    schedule 22.11.2015
comment
Было бы очень полезно рассмотреть в ответах общий вопрос, а не (только) ограничения GC...   -  person matanster    schedule 22.11.2015
comment
Компилятор не помечает ничего подходящего для сборки мусора. Вещи собираются мусором, когда на них больше нет ссылок, а не по указанию компилятора. Вот почему правильный ответ отвечает о сборщике мусора и его ограничениях.   -  person The Archetypal Paul    schedule 22.11.2015
comment
Таким образом, программы Scala, написанные в неизменяемом шаблоне, в целом описанном выше, по своей сути неадекватны для обработки больших данных любым эффективным (для памяти) способом. Вы уверены, что разыменование никоим образом не может быть намекнуто байт-кодом? что, если цепочка кода в примере была, например. разделить на функции?   -  person matanster    schedule 22.11.2015


Ответы (2)


Прежде всего: фактическое освобождение неиспользуемой памяти происходит всякий раз, когда JVM GC сочтет это необходимым. Так что scalac ничего не может с этим поделать.

Единственное, что может сделать scalac, — это установить для ссылок значение null не только тогда, когда они выходят за пределы области действия, но и как только они больше не используются.

В основном

val huge: HugeObjectType
val derivative1 = huge.map(_.x)
huge = null // inserted by scalac
val derivative2 = derivative1.groupBy(....)
derivative1 = null // inserted by scalac

Согласно этой ветке на scala-internals, в настоящее время < strong>не этого делать, и последняя JVM не обеспечивает спасение. См. сообщение хакера scalac Grzegorz Kossakowski и остальную часть этой темы.

Для метода, который оптимизируется JIT-компилятором JVM, JIT-компилятор обнуляет ссылки как можно скорее. Однако для основного метода, который выполняется только один раз, JVM никогда не будет пытаться полностью его оптимизировать.

Тема, указанная выше, содержит довольно подробное обсуждение темы и всех компромиссов.

Обратите внимание, что в типичных средах обработки больших данных, таких как apache spark, значения, с которыми вы работаете, не являются прямыми ссылками на данные. Таким образом, в этих фреймворках время жизни ссылок обычно не является проблемой.

В приведенном выше примере все промежуточные значения используются ровно один раз. Таким образом, простое решение состоит в том, чтобы просто определить все промежуточные результаты как определения.

def huge: HugeObjectType
def derivative1 = huge.map(_.x)
def derivative2 = derivative1.groupBy(....)
val result = derivative2.<some other transform>

Другой, но очень мощный подход — использование итераторов! связывание функций, таких как map и filter, над итератором обрабатывает их поэлементно, в результате чего никакие промежуточные коллекции никогда не материализуются... что очень хорошо соответствует сценарию! это не поможет с такими функциями, как groupBy, но может значительно уменьшить выделение памяти для предыдущих функций и подобных. Кредиты Саймону Шаферу из вышеупомянутого.

person Rüdiger Klaehn    schedule 22.11.2015
comment
Спасибо за то, что так четко изложили суть и детали вопроса, а также за то, что предоставили наиболее актуальную в настоящее время ветку группы Google! Я добавил о потоковых вычислениях с итераторами в ответ, прежде чем отметить его принятым. Defs подверглись резкой критике в другом ответе, не совсем уверен, насколько эта критика весит по отношению к предотвращению разрастания памяти с помощью большое количество данных... - person matanster; 24.11.2015
comment
Не совсем уверен, что представляет собой основной метод, который выполняется только один раз в данном контексте. Я предполагаю, что мы говорим не только об одной основной функции приложения ... есть ли у кого-нибудь хорошая ссылка для окончательного определения? - person matanster; 24.11.2015
comment
Я серьезно сомневаюсь, что дополнительный файл класса что-то изменит в такой ситуации. И да, defs будет оцениваться при каждом использовании. В этом-то и дело. - person Rüdiger Klaehn; 24.11.2015

derivative1 будет удален сборщиком мусора, как только он выйдет из области видимости (и на него нет других ссылок). Чтобы это произошло как можно скорее, сделайте следующее:

val huge: HugeObjectType
val derivative2 = {
    val derivative1 = huge.map(_.x)
    derivative1.groupBy(....)
}

Это также лучше с точки зрения читабельности кода, поскольку очевидно, что единственная причина существования derivative1 — это derivative2, и что он больше не используется после закрывающей скобки.

person Rok Kralj    schedule 22.11.2015
comment
В предыдущем ответе упоминаются определения, есть ли вообще какие-либо различия, кроме синтаксиса и вашего взгляда на такой стиль кодирования, нагруженный продолжительностью жизни? Я бы с удовольствием принял этот стиль для некоторых случаев... не обязательно для более длинных цепочек манипуляций. - person matanster; 22.11.2015
comment
defs опасны, они пересчитываются при каждом доступе и вызывают создание дополнительных файлов .class. И я предлагаю вам вместо этого принять этот стиль, ограничив область действия. Это имеет дополнительное преимущество, заключающееся в том, что программисту легче думать, так что он может быть уверен, что он покончил с этим значением, и что он не будет тайно использоваться через 100 строк пути исполнения. - person Rok Kralj; 23.11.2015
comment
Хорошо осознавать это. Тем не менее, это будет один блок на преобразование в общем случае более длинной цепочки преобразований данных. Общая цель состояла в том, чтобы поддерживать более или менее постоянное потребление памяти на протяжении всей цепочки преобразований, предполагаемых сборок мусора. Каждый вводимый блок очищает эту цель только для одного преобразования... так что все еще полезно, хотя и немного ужасно с точки зрения стиля кода в длинных цепочках преобразования данных. - person matanster; 23.11.2015
comment
Кстати, почему меня так сильно волнуют несколько дополнительных файлов классов? :) - person matanster; 23.11.2015