分享您的想法,参与 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: class file user$eval1032 中无效的方法代码长度为 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]
(计数 (字符串输出 (打印 (宏展开 form))))))

用户=> (formsize '(doseq [x (range 10)] (打印 x)))
652
用户=> (formsize '(doseq [x (range 10) y (range 10)] (打印 x y)))
1960
用户=> (formsize '(doseq [x (range 10) y (range 10) z (range 10)] (打印 x y z)))
4584
用户=> (formsize '(doseq [x (range 10) y (range 10) z (range 10) w (range 10)] (打印 x y z w)))
9947
用户=> (formsize '(doseq [x (range 10) y (range 10) z (range 10) w (range 10) p (range 10)] (打印 x y z w p)))
20997
`

以下是 2014 年 6 月 25 日的 Ghadi 补丁 doseq.patch 之后的相同表达式的结果

用户=> (formsize '(doseq [x (range 10)] (打印 x))) 93 用户=> (formsize '(doseq [x (range 10) y (range 10)] (打印 x y))) 170 用户=> (formsize '(doseq [x (range 10) y (range 10) z (range 10)] (打印 x y z))) 247 用户=> (formsize '(doseq [x (range 10) y (range 10) z (range 10) w (range 10)] (打印 x y z w))) 324 用户=> (formsize '(doseq [x (range 10) y (range 10) z (range 10) w (range 10) p (range 10)] (打印 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 "超级")

;; 大型序列,少量绑定: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]
(时间 (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毫秒"

`

也没有对于内建函数如 (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在原始的seq之前,除了对于小的seqs

0

评论人员:gshayban

以下形式的代码与这个补丁不兼容

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

因为go宏不会跨越fn边界。我所知的仅此一个这样的代码是 core.async/mapcat**,这是一个支持标记为废弃函数的私有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
它为序列绑定使用显式的堆栈。
目前还没有处理任何分块或修改。

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