2024年Clojure调查!中分享您的想法。

欢迎!请查看关于页面以获取有关该功能的一些更多信息。

0
Clojure

重要性能注意 新的实现对自定义可归约但未分批的集合更快,且对于大量绑定也更快。原始实现针对分批集合进行了手动调优,在大块集合/较小绑定计数场景中获胜,这可能是由于reduce的函数调用/返回跟踪开销所致。详细信息请见注释。
筛查者
补丁 doseq.patch

`
user=> (def a1 (range 10))

'user/a1

user=> (doseq [x1 a1 x2 a1 x3 a1 x4 a1 x5 a1 x6 a1 x7 a1 x8 a1] (do))
CompilerException java.lang.ClassFormatError: Invalid method Code length 69883 in class file user$eval1032, compiling:(NO_SOURCE_PATH:2:1)
`

虽然这个例子很荒谬,但我们遇到过几次这样的问题。当你只有几行代码时,突然出现代码长度错误,这相当令人惊讶。

15 个答案

0

评论由:hiredman 创建

在jdk 1.8.0和clojure 1.6下可重现

0

评论由:bronsa 创建

对此可能的一个修复方案是让doseq生成中间函数,就像for那样,而不是直接展开所有代码。

0

评论由:gshayban 创建

Existing doseq handles chunked-traversal internally, deciding the
mechanics of traversal for a seq.  In addition to possibly conflating
concerns, this is causing a code explosion blowup when more bindings are
added, approx 240 bytes of bytecode per binding (without modifiers).

This approach redefs doseq later in core.clj, after protocol-based
reduce (and other modern conveniences like destructuring.)

It supports the existing :let, :while, and :when modifiers.

New is a stronger assertion that modifiers cannot come before binding
expressions.  (Same semantics as let, i.e. left to right)

valid:  (link: x coll :when (foo x))
invalid: (link: :when (foo x) x coll)

This implementation does not suffer from the code explosion problem.
About 25 bytes of bytecode + 1 fn per binding.

Implementing this without destructuring was not a party, luckily reduce
is defined later in core.
0

由 jafingerhut 发布的评论:

对于审查此补丁的任何人,请注意,在文件 test/clojure/test_clojure/for.clj 中已经对 doseq 函数的正确功能进行了大量测试。这可能不是很明显,但定义 'for' 的每个使用 deftest-both 的测试,都是对 'for' 和 'doseq' 的测试。

关于 doseq 的当前实现:问题不仅仅在于每次绑定字节过多,还在于代码大小随着每个额外绑定的增加而翻倍。请参见以下结果,这些结果是衡量宏展开形式的大小,而不是字节码大小,但这两者在这里应该有相对线性的关系。

`
(defn formsize [form]
(count (with-out-str (print (macroexpand form)))))

user=> (formsize '(doseq [x (range 10)] (print x)))
652
user=> (formsize '(doseq [x (range 10) y (range 10)] (print x y)))
1960
user=> (formsize '(doseq [x (range 10) y (range 10) z (range 10)] (print x y z)))
4584
user=> (formsize '(doseq [x (range 10) y (range 10) z (range 10) w (range 10)] (print x y z w)))
9947
user=> (formsize '(doseq [x (range 10) y (range 10) z (range 10) w (range 10) p (range 10)] (print x y z w p)))
20997
`

以下是 2014 年 6 月 25 日提交的 Ghadi 补丁 doseq.patch 以后这些表达式的结果:

user=> (formsize '(doseq [x (range 10)] (print x))) 93 user=> (formsize '(doseq [x (range 10) y (range 10)] (print x y))) 170 user=> (formsize '(doseq [x (range 10) y (range 10) z (range 10)] (print x y z))) 247 user=> (formsize '(doseq [x (range 10) y (range 10) z (range 10) w (range 10)] (print x y z w))) 324 user=> (formsize '(doseq [x (range 10) y (range 10) z (range 10) w (range 10) p (range 10)] (print x y z w p))) 401

最好也能看到包含和不包含此补丁的性能结果。

0

stu 发布的评论:

以下测试中,新实现称为 "doseq2",而原始实现称为 "doseq"。

`
(def hund (into [] (range 100)))
(def ten (into [] (range 10)))
(def arr (int-array 100))
(def s "superduper")

;; 大序列,很少绑定:doseq2 失败
(dotimes [_ 5]
(time (doseq [a (range 100000000)])))
;; 1.2 秒

(dotimes [_ 5]
(time (doseq2 [a (range 100000000)])))
;; 1.8 秒

;; 小的未分块的 reducible,少绑定:doseq2 胜利
(dotimes [_ 5]
(time (doseq [a s b s c s])))
;; 0.5 秒

(dotimes [_ 5]
(time (doseq2 [a s b s c s])))
;; 0.2 秒

(dotimes [_ 5]
(time (doseq [a arr b arr c arr])))
;; 40 毫秒

(dotimes [_ 5]
(time (doseq2 [a arr b arr c arr])))
;; 8 毫秒

;; 小的分块 reducible,少绑定:doseq2 失败
(dotimes [_ 5]
(time (doseq [a hund b hund c hund])))
;; 2 毫秒

(dotimes [_ 5]
(time (doseq2 [a hund b hund c hund])))
;; 8 毫秒

;; 更多的绑定:doseq2 的优势越来越大
(dotimes [_ 5]
(time (doseq [a ten b ten c ten d ten ])))
;; 2 毫秒

(dotimes [_ 5]
(time (doseq2 [a ten b ten c ten d ten ])))
;; 0.4 毫秒

(dotimes [_ 5]
时间(doseq [a 十 b 十 c 十 d 十 e 十]))
;; 18 毫秒

(dotimes [_ 5]
时间(doseq2 [a 十 b 十 c 十 d 十 e 十]))
;; 1 毫秒
`

0
by

评论由:gshayban 创建

哎呀,我无法重现你的结果。

我不确定你是否使用 lein 测试,在什么平台上,什么 jvm opts。

我们能否使用这个小执行器来测试,而不是直接针对 clojure.jar?我已经附上了执行器及其两次运行结果(一个使用默认堆,另一个 3GB 使用 G1GC)

我还添加了中等和小的范围(range)。

据观察,doseq2 在所有情况(除了小范围)中表现都优于其他。使用 criterium 显示了更大的性能差距,偏向于 doseq2。

我将结果并排放置以便更轻松地查看。

`
core/doseq doseq2
"执行时间: 1610.865146 毫秒" "执行时间: 2315.427573 毫秒"
"执行时间: 2561.079069 毫秒" "执行时间: 2232.479584 毫秒"
"执行时间: 2446.674237 毫秒" "执行时间: 2234.556301 毫秒"
"执行时间: 2443.129809 毫秒" "执行时间: 2224.302855 毫秒"
"执行时间: 2456.406103 毫秒" "执行时间: 2210.383112 毫秒"

;; 中等范围,少量绑定
core/doseq doseq2
"执行时间: 28.383197 毫秒" "执行时间: 31.676448 毫秒"
"执行时间: 13.908323 毫秒" "执行时间: 11.136818 毫秒"
"执行时间: 18.956345 毫秒" "执行时间: 11.137122 毫秒"
"执行时间: 12.367901 毫秒" "执行时间: 11.049121 毫秒"
"执行时间: 13.449006 毫秒" "执行时间: 11.141385 毫秒"

;; 小范围,少量绑定
core/doseq doseq2
"执行时间: 0.386334 毫秒" "执行时间: 0.372388 毫秒"
"执行时间: 0.10521 毫秒" "执行时间: 0.203328 毫秒"
"执行时间: 0.083378 毫秒" "执行时间: 0.179116 毫秒"
"执行时间: 0.097281 毫秒" "执行时间: 0.150563 毫秒"
"执行时间: 0.095649 毫秒" "执行时间: 0.167609 毫秒"

;; 小型非分块可减少,少量绑定
core/doseq doseq2
"执行时间: 2.351466 毫秒" "执行时间: 2.749858 毫秒"
"执行时间: 0.755616 毫秒" "执行时间: 0.80578 毫秒"
"执行时间: 0.664072 毫秒" "执行时间: 0.661074 毫秒"
"执行时间: 0.549186 毫秒" "执行时间: 0.712239 毫秒"
"执行时间: 0.551442 毫秒" "执行时间: 0.518207 毫秒"

core/doseq doseq2
"执行时间: 95.237101 毫秒" "执行时间: 55.3067 毫秒"
"执行时间: 41.030972 毫秒" "执行时间: 30.817747 毫秒"
"执行时间: 42.107288 毫秒" "执行时间: 19.535747 毫秒"
"执行时间: 41.088291 毫秒" "执行时间: 4.099174 msecs"
"执行时间: 41.03616 毫秒" "执行时间: 4.084832 毫秒"

;; 小型分块可减少,少量绑定
core/doseq doseq2
"执行时间: 31.793603 毫秒" "执行时间: 40.082492 毫秒"
"执行时间: 17.302798 毫秒" "执行时间: 28.286991 毫秒"
"执行时间: 17.212189 毫秒" "执行时间: 14.897374 毫秒"
"执行时间: 17.266534 毫秒" "执行时间: 10.248547 毫秒"
"执行时间: 17.227381 毫秒" "执行时间: 10.022326 毫秒"

;; 更多绑定
core/doseq doseq2
"执行时间: 4.418727 msecs" "执行时间: 2.685198 msecs"
"执行时间: 2.421063 msecs" "执行时间: 2.384134 msecs"
"执行时间: 2.210393 msecs" "执行时间: 2.341696 msecs"
"执行时间: 2.450744 msecs" "执行时间: 2.339638 msecs"
"执行时间: 2.223919 msecs" "执行时间: 2.372942 msecs"

core/doseq doseq2
"执行时间: 28.869393 msecs" "执行时间: 2.997713 msecs"
"执行时间: 22.414038 msecs" "执行时间: 1.807955 msecs"
"执行时间: 21.913959 msecs" "执行时间: 1.870567 msecs"
"已过时间: 22.357315 msecs" "已过时间: 1.904163 msecs"
"已过时间: 21.138915 msecs" "已过时间: 1.694175 msecs"
`

0

评论由:gshayban 创建

基准测试中包含空doseq体以隔离遍历开销是很好的。然而,这仅仅代表实际真实代码的0%。

至少对于第一个基准(大块seq),添加一些微小的工作量并没有显著改变结果。至于 (map str (link: a)) 也没有改变。

`
(range 10000000) => (map str [a])
core/doseq
"已过时间: 586.822389 msecs"
"已过时间: 563.640203 msecs"
"已过时间: 369.922975 msecs"
"已过时间: 366.164601 msecs"
"已过时间: 373.27327 msecs"
doseq2
"已过时间: 419.704021 msecs"
"已过时间: 371.065783 msecs"
"已过时间: 358.779231 msecs"
"已过时间: 363.874448 msecs"
"已过时间: 368.059586 msecs"

`

也不是内建函数如 (inc a)

`

(range 10000000)
core/doseq
"已过时间: 317.091849 msecs"
"已过时间: 272.360988 msecs"
"已过时间: 215.501737 msecs"
"已过时间: 206.639181 msecs"
"已过时间: 206.883343 msecs"
doseq2
"已过时间: 241.475974 msecs"
"已过时间: 193.154832 msecs"
"已过时间: 198.757873 msecs"
"已过时间: 197.803042 msecs"
"已过时间: 200.603786 msecs"
`

我想看到基于reduce的doseq仍然在原始版之前,除了小的seqs

0

评论由:gshayban 创建

以下形式的表单将无法使用此补丁工作

(go (doseq [c chs] (>! c :foo)))

因为go宏不能跨越fn边界。我所知的唯一此类代码是core.async/mapcat**,一个支持标记为过时的fn的私有fn。

0

评论由:gshayban 创建

我看到#'clojure.core/run!刚刚被添加,它也有类似的限制

0

评论者:richhickey

请考虑Ghadi的反馈,特别是关于闭包的反馈。

0

评论由:gshayban 创建

在go形式下,由于控制流的数量,doseq(链接:1)的当前扩展并不理想。状态机中有14个状态,而循环/递归中只有7个。

(链接:1) (go ... doseq) 与 (go ... loop/recur) 的宏扩展对比
https://gist.github.com/ghadishayban/639009900ce1933256a1

0

评论由:bronsa 创建

相关:CLJ-77

0

评论由:bronsa 创建

解决此问题的通用方案是对方法进行自动拆分,当方法过大时,例如使用https://bitbucket.org/sperber/asm-method-size

0

评论由:gshayban 创建

示例doseq实现和不受指数字节码增长影响的宏扩展。它也不使用任何lambda,因此适用于core.async。
https://gist.github.com/ghadishayban/fe84eb8db78f23be68e20addf467c4d4
它为seqs/bindings使用了显式栈。
目前它还没有处理任何分块或修饰符。

0
参考:https://clojure.atlassian.net/browse/CLJ-1322(由arcatan报告)
...