2013-06-26 4 views
9

За исключением добавленной многословности, есть ли другие веские причины, почему нельзя утверждать, что каждая переменная экземпляра должна быть лениво инициализирована?Почему бы не сделать каждую переменную экземпляра Scala лениво инициализированной?

+2

Что касается скрытых расходов: http://stackoverflow.com/q/3041253/2390083 – Beryllium

+2

И скрытые расходы будут все хуже: http://stackoverflow.com/a/17329465/175251 –

ответ

20

Прежде всего: если что-то пойдет не так в инициализации ленивого val (например, доступ к внешнему ресурсу, которого не существует), вы заметите его только при первом доступе к val, тогда как с нормальным val вы заметят, как только объект будет построен. Вы также можете иметь циклические зависимости в ленивых vals, которые приведут к тому, что класс не работает вообще (один из страшных NullPointerExceptions), но вы узнаете только при первом доступе к одному из подключенных ленивых vals.

Так ленивые деньги делают программу менее детерминированной, что всегда плохо.

Во-вторых: накладные расходы во время выполнения связаны с ленивым значением. Lazy val в настоящее время реализуется частной битовой маской (int) в классе с использованием lazy vals (один бит для каждого ленивого val, поэтому, если у вас более 32 ленивых vals будут два битмакса и т. Д.)

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

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

Конечно, для очень маленького класса также могут быть важны служебные данные памяти битмаски.

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

Edit: вот пример кода, который иллюстрирует недетерминизм вы можете получить, если вы используете ленивые Vals:

class Test { 
    lazy val x:Int = y 
    lazy val y:Int = x 
} 

Вы можете создать экземпляр этого класса без каких-либо проблем, но как только вы получаете доступ либо x или y вы получите StackOverflow. Это, конечно, искусственный пример. В реальном мире у вас гораздо более длительные и неочевидные циклы зависимостей.

Это сеанс консоли scala, используя: javap, который иллюстрирует накладные расходы времени выполнения ленивого val. Первый нормальный вал:

scala> class Test { val x = 0 } 
defined class Test 

scala> :javap -c Test 
Compiled from "<console>" 
public class Test extends java.lang.Object implements scala.ScalaObject{ 
public int x(); 
    Code: 
    0: aload_0 
    1: getfield #11; //Field x:I 
    4: ireturn 

public Test(); 
    Code: 
    0: aload_0 
    1: invokespecial #17; //Method java/lang/Object."<init>":()V 
    4: aload_0 
    5: iconst_0 
    6: putfield #11; //Field x:I 
    9: return 

} 

А теперь ленивый вал:

scala> :javap -c Test 
Compiled from "<console>" 
public class Test extends java.lang.Object implements scala.ScalaObject{ 
public volatile int bitmap$0; 

public int x(); 
    Code: 
    0: aload_0 
    1: getfield #12; //Field bitmap$0:I 
    4: iconst_1 
    5: iand 
    6: iconst_0 
    7: if_icmpne 45 
    10: aload_0 
    11: dup 
    12: astore_1 
    13: monitorenter 
    14: aload_0 
    15: getfield #12; //Field bitmap$0:I 
    18: iconst_1 
    19: iand 
    20: iconst_0 
    21: if_icmpne 39 
    24: aload_0 
    25: iconst_0 
    26: putfield #14; //Field x:I 
    29: aload_0 
    30: aload_0 
    31: getfield #12; //Field bitmap$0:I 
    34: iconst_1 
    35: ior 
    36: putfield #12; //Field bitmap$0:I 
    39: getstatic #20; //Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; 
    42: pop 
    43: aload_1 
    44: monitorexit 
    45: aload_0 
    46: getfield #14; //Field x:I 
    49: ireturn 
    50: aload_1 
    51: monitorexit 
    52: athrow 
    Exception table: 
    from to target type 
    14 45 50 any 

public Test(); 
    Code: 
    0: aload_0 
    1: invokespecial #26; //Method java/lang/Object."<init>":()V 
    4: return 

} 

Как вы можете видеть, нормальный вал аксессор очень короткий и, безусловно, будет встраиваемый, в то время как ленивый вал аксессор довольно сложный и (что наиболее важно для параллелизма) включает в себя синхронизированный блок (команды monitorenter/monitorexit). Вы также можете увидеть дополнительное поле, генерируемое компилятором.

7

Во-первых, речь идет о lazy val s («константы» Скалы), а не ленивые переменные (которых я не думаю о существовании).

Две причины были бы ремонтопригодность и эффективность, особенно в контексте полей класса:

Эффективность: выгода, не ленивой инициализации является то, что вы контролируете, где это происходит. Представьте себе структуру типа fork-join, в которой вы создаете несколько объектов в рабочих потоках, а затем передаете их в центральную обработку. С нетерпением eval инициализация выполняется на рабочих потоках. С ленивым eval это делается на основной теме, потенциально создавая узкое место.

ремонтопригодность: если все значения лениво инициализированы, и ваша программа взрывает, вы получаете трассировки стеки, локализованные в совершенно ином контексте, чем инициализация экземпляра, возможно, в другом потоке.

Есть также, конечно, расходы, связанные с использованием языка (я вижу, что @Beryllium опубликовал один пример), но я не достаточно компетентен, чтобы обсудить их.

+0

Wouldn «Имеет ли смысл делать поколение, а также обрабатывать рабочие потоки, а затем возвращать их в главный поток? Или, если требуется взаимодействие с объектом, рабочие потоки возвращают свои объекты другому рабочему потоку, который будет выполнять обработку, а не основной поток, так что главный поток не должен беспокоиться об управлении большей частью рабочих потоков, средний менеджмент (так сказать). – JAB

+0

Вот что я имею в виду. Чтобы разработать, большое количество алгоритмов имеет распределенный обрабатывающий элемент ** и ** централизованный обрабатывающий элемент - иначе * закон Амдаля * не будет существовать :). Одним из тривиальных примеров является этап слияния Mergesort. Конечно, это может показаться очевидным, но когда вы потенциально находитесь в мышлении «все, что у меня есть, это молот», это реальная ошибка, с которой вы можете попасть. –

0

Если я прочитал ваш код, и вы использовали ленивый, я бы сжег время, спрашивая, ПОЧЕМУ ВЫ ИСПОЛЬЗУЕТЕ ленивую инициализацию, которая, вероятно, является самой дорогой ценой ленивых в дополнение к штрафам за производительность.

Теперь, когда вы должны думать о отложенной инициализации (и аналогично Streams, которые я причисляю) это:

Циклическая зависимость: где одна переменная зависит от другой инициализации и/или наоборот. Бесконечные наборы: потоки позволяют вам найти первые 1000 простых чисел, не требуя знать, сколько действительных чисел может отображаться.

Я уверен, что есть пара других - это большие, которые я вижу.

Просто помните, что ленивый val - это как def, который оценивается ровно один раз и знает, что вы должны использовать его только тогда, когда вам это действительно нужно, иначе он запутает другого разработчика, когда спросит, почему он ленив?