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

2022-09-03 07:42:47
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的源代码)吗?

如果这个答案击中了我遇到的问题,那么(a)为什么分块意味着它呈指数级增长而不是线性增长?(b)作为一名程序员,这对我有什么影响?例如,此行为是否众所周知且有意为之?我是否应该避免在超过 3 个或 4 个绑定的任何情况下使用?这与使用 和 相比如何?doseqfordoall

也许相关:

Clojure doseq 生成大量代码

方法代码太大!使用 ASM 的异常


答案 1

您看到的是优化的令人讨厌的副作用,该优化被放入宏的实现中以处理输入中的分块序列。您链接的问题的答案正确描述了根本原因,但并没有阐明为什么事情会以这种方式发生。doseq

该实现在内部使用一个函数,该函数以递归方式构建一系列嵌套构造,每个嵌套构造对应于 中的每个绑定级别。在此实现的朴素、未优化版本中,每个级别的循环将简单地运行其主体,然后使用其 seq 的值进行调用。类似如下的内容:doseqlooploopdoseqrecurnext

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

但是,如果该 seq 恰好是一个分块序列,这将导致不必要的创建大量中间 seq 对象,这些对象从未在循环主体之外使用过。已经做出的选择化是将一个分支放在里面,一个分支来处理分块序列,另一个分支处理非分块序列。循环体在每个分支之间复制。如果循环体恰好是嵌套循环,则您可以看到代码大小的指数增长是如何发生的 - 扩展代码的每个级别的循环都有两个子循环。doseqifloop

因此,为了回答您的问题,我不会确切地说代码大小的爆炸式增长是有意的,但这是 设计行为的结果。它只是不是为处理深度嵌套的循环而设计的,在野外,我从未见过它与超过一两个级别的绑定一起使用。doseq

您可以使用 and 的组合重现深度嵌套的语义(不要使用,因为这会不必要地保留 seq 的头部)。这将允许您处理任何级别的嵌套,如果您碰巧在紧密循环中运行分块序列,则性能会受到轻微但可衡量的影响。doseqfordorundoall

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))

推荐