【问题标题】:For vs. Doseq (and Method Code Too Large)For vs. Doseq(和方法代码太大)
【发布时间】:2014-10-14 06:02:33
【问题描述】:
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

【问题讨论】:

  • 看起来你的doseq中的每一个额外绑定都会使生成的代码的大小加倍(我用no.disassemble尝试过)。不知道为什么。
  • +1 哇!不错的收获!我真的很想知道更多。
  • 我天真地期望在fordoseq 之间,for 会产生更大的方法。如果doseq 存在这种低效率,那么我很想知道在什么复杂度阈值下使用(def ignore (doall (for [stuff] (side-effect))) 而不是更自然的(doseq [stuff] (side-effect)) 可以获得更好的性能。
  • @DiegoBasch,我刚刚检查了 no.disassemble。这很酷。感谢您的参考。 :)
  • 您链接的第一个问题中接受的答案解释了为什么会发生这种情况。 doseq 的宏扩展有一个分支来处理每个绑定的 seq 表达式的分块 seq,这意味着每个新绑定的扩展代码大小加倍。

标签: java clojure java-bytecode-asm


【解决方案1】:

您所看到的是优化的一个令人讨厌的副作用,该优化被放入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 所做的优化是在loop 内放置一个if,其中一个分支处理分块序列,一个分支处理非分块序列。循环体在每个分支之间重复。如果循环体恰好是一个嵌套循环,那么您可以看到代码大小是如何呈指数增长的——展开代码的每一级循环都有两个子循环。

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

您可以使用fordorun 的组合重现深度嵌套的doseq 的语义(不要使用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))
    

    【讨论】:

    • 我认为您的回答可能不错,但提供更多详细信息会有所帮助
    • 这并不能真正回答问题。如果您有其他问题,可以点击 进行提问。一旦你有足够的reputation,你也可以add a bounty 来引起对这个问题的更多关注。
    • @vyegorov,实际上他的回答详细说明了找到解决方法所需的所有信息。我并不是完全在寻找解决方法,但他的回答肯定比您粘贴的评论更有帮助。
    猜你喜欢
    • 2013-02-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-03-10
    • 1970-01-01
    • 2020-02-08
    相关资源
    最近更新 更多