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))
编译器异常 java.lang.ClassFormatError: 不合法的方法代码长度 69883 在类文件 user$eval1032,正在编译:(NO_SOURCE_PATH:2:1)
`

虽然这个例子很愚蠢,但我们确实遇到了几次这种问题。只有几行代码时突然出现代码长度错误还是挺意外的。

15 回复

0

评论者:hiredman

在 jdk 1.8.0 和 clojure 1.6 下重现

0

评论者:bronsa

修复此问题的方法之一是让 doseq 生成类似于 for 的中间函数,而不是直接展开所有代码。

0
by

评论者: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
by

评论者: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
`

以下是Ghadi于2014年6月25日提交的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
by

评论者: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
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
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 优于原始版本,除非对于小的 seq

0
by

评论者:gshayban

以下形式的代码不会与这个补丁一起工作

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

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

0
by

评论者:gshayban

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

0
by

评论者:richhickey

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

0
by

评论者: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 报告)
...