2024 Clojure状态调查!分享你的想法。

欢迎!请参阅关于页面以了解更多关于如何使用本网站的信息。

0
Clojure

重要性能注意 新的实现对于自定义递归但未分块的集合更快,对于大量的绑定也更快。原始实现针对分块集合手工调优,在更大的分块集合/较小的绑定数量场景中获胜,这可能是由于reducing的fn调用/返回跟踪开销导致的。详细信息请参见注释。
筛查者
补丁 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))
编译器异常 java.lang.ClassFormatError: class file user$eval1032中的Invalid method Code length 69883 在编译:(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 的正确功能执行了许多测试。虽然乍看之下并不明显,但每个使用 deftest-both 定义的 'for' 测试实际上也是对 '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 LOSES
(dotimes [_ 5]
(time (doseq [a (range 100000000)])))
;; 1.2 秒

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

;; 小型未分块可还原序列,少量绑定: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 毫秒

;; 小型分块可还原序列,少量绑定:doseq2 LOSES
(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]
(时间 (doseq [a ten b ten c ten d ten ]))
;; 2 毫秒

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

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

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

0
by

评论由:gshayban 提供

嗯,我无法复现你的结果。

我不确定你是否在使用 lein 测试,在哪个平台上,使用了哪些 JVM 选项。

我们可以使用这个小 harness 来测试,而不是直接针对 clojure.jar 吗?我已经附上了 harness 和两次运行的结果(一个默认堆,另一个 3GB 的 G1GC 堆)。

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

根据经验,我发现 doseq2 在所有情况下都优于 doseq,除了小的范围。使用 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 毫秒"
"已过时间: 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 毫秒" "已过时间: 2.685198 毫秒"
"已过时间: 2.421063 毫秒" "已过时间: 2.384134 毫秒"
"已过时间: 2.210393 毫秒" "已过时间: 2.341696 毫秒"
"已过时间: 2.450744 毫秒" "已过时间: 2.339638 毫秒"
"已过时间: 2.223919 毫秒" "已过时间: 2.372942 毫秒"

core/doseq doseq2
"已过时间: 28.869393 毫秒" "已过时间: 2.997713 毫秒"
"已过时间: 22.414038 毫秒" "已过时间: 1.807955 毫秒"
"已过时间: 21.913959 毫秒" "已过时间: 1.870567 毫秒"
"已用时间:22.357315 毫秒" "已用时间:1.904163 毫秒"
"已用时间:21.138915 毫秒" "已用时间:1.694175 毫秒"
`

0

评论由:gshayban 提供

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

至少对于第一个基准(大型分割序列),添加一些微小的工作量并没有显著改变结果。对于(map str (link: a))也是如此

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

`

对于内建操作符,如(inc a)

`

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

除了小的序列外,我仍然看到基于 reduce 的 doseq 在原始版本之前

0

评论由:gshayban 提供

以下形式的代码将无法与该补丁一起工作

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

因为 go 宏不会遍历函数边界。我所知道的唯一此类代码是 core.async/mapcat**,它是一个支持过期函数的私有函数。

0

评论由:gshayban 提供

我注意到刚刚增加了#'clojure.core/run!,它也有类似的限制

0

评论者:richhickey

请考虑 Ghadi 的反馈,特别是关于封闭的范围。

0

评论由:gshayban 提供

在go形式下,由于控制流的数量,当前doseq(链接:1)的扩展并不理想。状态机中有14个状态,而loop/recur中只有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](https://clojure.atlassian.net/browse/CLJ-1322)(由 arcatan 报告)
...