对于Doseq(和方法代码太大)

时间:2014-10-14 06:02:34

标签: java clojure java-bytecode-asm

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。我用谷歌搜索这个错误与Clojure几乎没有命中。

那么......到底是怎么回事?这个兔子洞有多深?这一行Clojure代码是否真的产生了一个> 65KB的方法(该值来自MethodWriter的源代码)?

如果this answer正在遇到我遇到的问题,那么(a)为什么分块意味着它呈指数增长而不是线性增长? (b)作为程序员,对我有什么影响?例如,这种行为是众所周知的吗?对于超过3或4个绑定的任何情况,我应该避免使用doseq吗?与使用fordoall相比,这相比如何?

也许相关:

Clojure doseq generates huge code

Method code too large! exception using ASM

2 个答案:

答案 0 :(得分:8)

您所看到的是优化的一个令人讨厌的副作用,该优化被放入doseq宏的实现中以处理输入中的chunked sequences。你正确联系的问题的答案描述了根本原因,但并没有充分阐明事情发生的原因。

doseq实现在内部使用一个函数,递归地构建一系列嵌套loop构造,loop中每个绑定级别一个doseq。在这个实现的一个天真的,未优化的版本中,每个级别的循环只是运行它的主体,然后使用recur的seq调用next。这些方面的东西:

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

如果seq恰好是一个分块序列,那么这将导致不必要地创建许多中间seq对象,这些对象从未在循环体外使用。 doseq所做的优化是将if置于loop内,其中一个分支处理分块序列,另一个处理非分块序列。循环体在每个分支之间重复。如果循环体恰好是嵌套循环,那么你可以看到代码大小的指数增长是如何发生的 - 扩展代码的每个级别的循环都有两个子循环。

所以,为了回答你的问题,我不能确切地说代码大小的爆炸是有意的,但它是doseq设计行为的结果。它只是设计用于处理深度嵌套的循环,而且在野外我从未见过它使用超过一个或两个级别的绑定。

您可以使用doseqfor的组合重现深层嵌套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"

答案 1 :(得分:1)

当我使用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))