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

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

0

性能注意事项新实现对于自定制的不可分解集合来说更快,对于较大数量的绑定也更高效。原始实现是针对可分集合进行了手动调优,适用于较大的可分集合/较小的绑定计数场景,这可能是由于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 正确功能进行了许多测试。可能不太明显,但每个使用 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 表现不佳
(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 失败
(dotimes [_ 5]
(时间 (doseq [a hund b hund c hund])))
;; 2毫秒

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

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

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

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

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

0

由 gshayban 发表的评论:

哼,我无法重现你的结果。

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

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

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

据我所知,除了小范围之外,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毫秒"
"耗时: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%。

至少对于第一个基准测试(大块 seq),添加少量工作并没有显著改变结果。对于 (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 毫秒"

`

也不适用于内建函数 Like (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 比原始版本要快,但对于小 seqs 除外

0

由 gshayban 发表的评论:

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

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

因为 go 宏不会遍历 fn 边界。我所知的唯一这样的代码是 core.async/mapcat**,它是一个支持已弃用函数的私有函数。

0

由 gshayban 发表的评论:

我注意到 'clojure.core/run!刚刚被添加,它具有类似的限制

0

评论者:richhickey

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

0
回答者

由 gshayban 发表的评论:

由于控制流的数量,当前在“go”形式下对“doseq”的扩展并不理想。状态机中的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
它为单个/绑定使用显式的堆栈。
它还没有处理任何分块或修饰。

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