2024 Clojure 状况调查 中分享您的想法!

欢迎!有关如何使用此功能的更多信息,请参阅 关于 页面。

0
Clojure

重要性能注意事项 新的实现在自定义可减少且未分块的集合上更快,在大量绑定上也更快。原始实现针对分块集合进行了手动调优,在较大的分块集合和较少数量的绑定情况下胜出,这可能是由于 reduce 的 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))
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]
(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]
(time (doseq [a ten b ten c ten d ten e ten])))
;; 18毫秒

(dotimes [_ 5]
(time (doseq2 [a ten b ten c ten d ten e ten])))
;; 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
by

评论者: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 毫秒"

`

或是对于内建函数如 (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 比原始 doseq 要快,除了小 seqs

0
by

评论者:gshayban

以下形式的表示不会与这个补丁一起正常工作

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

因为 go 宏不能穿越 fn 边界。我所知道的唯一此类代码是 core.async/mapcat**,这是一个支持函数标记为已废弃的私有 fn。

0
by

评论者:gshayban

我注意到 just 添加了 #’clojure.core/run!,它也有类似的限制

0
by

评论者:richhickey

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

0
by

评论者:gshayban

在 go 表达式下的 doseq (link: 1) 的当前扩展由于控制流数量较少而不够理想。状态机中有 14 个状态,而 loop/recur 中有 7 个。

(link: 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 报告)
...