Для vs. 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. Мой googling для этой ошибки с Clojure почти не вызывает хитов.

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

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

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

Clojure doseq генерирует огромный код

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

Ответы

Ответ 1

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

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

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

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

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

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

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"

Ответ 2

У меня была аналогичная проблема, когда я создавал свой собственный компилятор с помощью 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))