Для по сравнению с Doseq (и слишком большой код метода)

user=> (def r (range 1))
user=> (for [a r, b r, c r, d r, e r, f r, g r, h r :when (and (= 0 a) (not= 1 b))]
          (list a b c d e f g h))
((0 0 0 0 0 0 0 0))
user=> (doseq [a r, b r, c r, d r, e r, f r, g r, h r :when (and (= 0 a) (not= 1 b))]
          (println (list a b c d e f g h)))
CompilerException java.lang.RuntimeException: Method code too large!, compiling:(/tmp/form-init8346140986526777871.clj:1:1)

Похоже, это исходит от clojure.asm.MethodWriter . Мой поиск в Google этой ошибки с Clojure почти не дает результатов.

Итак... что, черт возьми, происходит? Насколько глубока эта кроличья нора? Действительно ли эта одна строка кода Clojure создает метод > 65 КБ (значение взято из источника MethodWriter)?

Если этот ответ затрагивает проблему, с которой я сталкиваюсь, то (а) почему фрагментация означает, что вместо этого она растет экспоненциально линейно? И (б) каковы последствия для меня как программиста? Например, является ли это поведение общеизвестным и преднамеренным? Следует ли мне избегать использования doseq в любой ситуации с более чем 3 или 4 привязками? Как это соотносится с использованием for и doall?

Возможно связано:

Дозаq Clojure генерирует огромный код

Код метода слишком велик! исключение с использованием ASM


person galdre    schedule 14.10.2014    source источник
comment
Похоже, что каждое дополнительное связывание в вашем doseq удваивает размер сгенерированного кода (я пробовал это с no.disassemble). Не уверен, почему.   -  person Diego Basch    schedule 14.10.2014
comment
+1 Ура! Хорошо поймал! Мне действительно интересно узнать больше об этом.   -  person Chiron    schedule 14.10.2014
comment
Мое наивное ожидание состояло бы в том, что между for и doseq будет for, который создаст более крупный метод. Если doseq имеет такую ​​неэффективность, то мне было бы интересно узнать, при каком пороге сложности вы получите лучшую производительность с (def ignore (doall (for [stuff] (side-effect))) вместо более естественного (doseq [stuff] (side-effect)).   -  person galdre    schedule 14.10.2014
comment
@DiegoBasch, я только что проверил no.disassemble. Это очень круто. Спасибо за ссылку. :)   -  person galdre    schedule 14.10.2014
comment
Принятый ответ на первый вопрос, который вы связали, объясняет, почему это происходит. Макрорасширение для дозыq имеет ветвь для обработки фрагментированных последовательностей для каждого связанного выражения последовательности, что означает, что расширенный код удваивается по размеру для каждой новой привязки.   -  person Alex    schedule 14.10.2014
comment
@ Алекс Честно говоря, я не нашел этот ответ очень полезным. Он показал, что doseq генерирует больше кода (ОП уже знал об этом) и что это связано с фрагментацией. Почему фрагментация означает, что он растет экспоненциально, а не линейно? Является ли это поведение хорошо известным и преднамеренным? Есть ли у него преимущество перед использованием for и doall?   -  person galdre    schedule 14.10.2014
comment
@galdre Если вы считаете принятый ответ на вопрос, который почти полностью повторяет ваш, неудовлетворительным, то, возможно, вам следует отредактировать свой вопрос, чтобы подчеркнуть ваши конкретные проблемы.   -  person Alex    schedule 14.10.2014
comment
Хорошо -- я сделаю это. Для протокола, однако, для меня было не совсем очевидно, что эти два вопроса были дубликатами, поэтому я указал их как возможно связанные. Боюсь, мое понимание Clojure под капотом довольно скудное.   -  person galdre    schedule 14.10.2014
comment
@Alex Должен ли я пометить это как дубликат? Меня не устраивает ответ на первоначальный вопрос (и я думаю, что мой вопрос более лаконичен и его легче найти), но я не уверен, каков правильный поток SO в этом случае. Награда за исходный вопрос?   -  person galdre    schedule 14.10.2014
comment
Думаю, можно оставить этот вопрос открытым. Вопрос в отредактированном виде достаточно отличается от оригинала.   -  person Alex    schedule 14.10.2014


Ответы (2)


То, что вы видите, является неприятным побочным эффектом оптимизации, которая была включена в реализацию макроса doseq для обработки разделил последовательности на вход. Ответ на вопрос, который вы связали, правильно описывает основную причину, но не проливает много света на то, почему все происходит именно так.

Реализация doseq внутренне использует функцию, которая рекурсивно создает серию вложенных конструкций loop, по одной loop для каждого уровня привязок в doseq. В простой, неоптимизированной версии этой реализации цикл на каждом уровне просто запускал бы свое тело, а затем вызывал бы recur со значением next для своей последовательности. Что-то в этом роде:

(loop [s (seq input)]
  (if s
    (do (run-body (first s))
        (recur (next s)))))

Однако, если эта последовательность окажется фрагментированной последовательностью, это приведет к ненужному созданию большого количества промежуточных объектов последовательности, которые никогда не используются вне тела цикла. Оптимизация, сделанная doseq, состоит в том, чтобы поместить if внутри loop с одной ветвью для обработки секвенированных последовательностей и одной для обработки нефрагментированных последовательностей. Тело цикла дублируется между каждой ветвью. Если тело цикла оказывается вложенным циклом, то вы можете видеть, как происходит экспоненциальное увеличение размера кода — цикл на каждом уровне расширенного кода имеет два дочерних цикла.

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

Вы можете воспроизвести семантику глубоко вложенного doseq с комбинацией for и dorun (не используйте doall, так как это без необходимости сохраняет заголовок последовательности). Это позволит вам обрабатывать любой уровень вложенности с небольшим, но измеримым падением производительности, если вам случится выполнять фрагментированную последовательность в узком цикле.

user> (time (doseq [x (range 10000) y (range 10000)] (* x y)))
"Elapsed time: 2933.543178 msecs"

user> (time (dorun (for [x (range 10000) y (range 10000)] (* x y))))
"Elapsed time: 5560.90003 msecs"
person Alex    schedule 14.10.2014

У меня была похожая проблема, когда я создавал свой собственный компилятор с использованием java.

Я объявил очень большую матрицу. Для меня решение было разделено на небольшую матрицу. Это просто предложение, возможно, вы можете сделать что-то подобное, например:

(def r (range 1))
(defn foo [a b c d]
   (doseq [e r, f r, g r, h r] (println "Hi")))
(doseq [a r, b r, c r, d r :when (and (= 0 a) (not= 1 b))]
   (foo a b c d))
person Gustavo Rossi Muller    schedule 14.10.2014
comment
Я думаю, что ваш ответ может быть хорошим, но было бы полезно предоставить более подробную информацию. - person StormeHawke; 14.10.2014
comment
Это на самом деле не отвечает на вопрос. Если у вас есть другой вопрос, вы можете задать его, нажав Задать вопрос. Вы также можете добавить вознаграждение, чтобы привлечь больше внимания к этому вопросу, как только у вас будет достаточно репутация. - person vyegorov; 14.10.2014
comment
@vyegorov, на самом деле в его ответе изложена вся информация, необходимая для поиска обходного пути. Я точно не ищу обходной путь, но его ответ, безусловно, был более полезным, чем ваши стандартные обзоры. - person galdre; 14.10.2014