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))
编译器异常 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那样生成中间fn,而不是直接展开所有代码。

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
`

以下是 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

评论者: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]
时间(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 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

评论者:gshayban

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

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

我们可以直接使用这个小小的 harness 来测试,而不是直接针对 clojure.jar 吗?我已经附加了这个 harness 和两次运行结果(一个使用默认堆,另一个使用 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 msecs” “经过时间:2.685198 msecs”
“经过时间:2.421063 msecs” “经过时间:2.384134 msecs”
“经过时间:2.210393 msecs” “经过时间:2.341696 msecs”
“经过时间:2.450744 msecs” “经过时间:2.339638 msecs”
“经过时间:2.223919 msecs” “经过时间:2.372942 msecs”

core/doseq doseq2
“经过时间:28.869393 msecs” “经过时间:2.997713 msecs”
"经过时间: 22.414038 毫秒" "经过时间: 1.807955 毫秒"
"经过时间: 21.913959 毫秒" "经过时间: 1.870567 毫秒"
"经过时间: 22.357315 毫秒" "经过时间: 1.904163 毫秒"
"经过时间: 21.138915 毫秒" "经过时间: 1.694175 毫秒"
`

0

评论者:gshayban

`
(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比原来的版本快,除了对于很小的seqs外

0

评论者:gshayban

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

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

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

0

评论者:gshayban

我看到刚添加了#’clojure.core/run!,它有类似的限制

0

评论者:richhickey

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

0

评论者:gshayban

由于控制流的数量,当前在go形式下的doreq展开并不理想。状态机中有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

示例doreq实现和没有指数级字节码增长的宏展开。它也不使用任何lambda,因此适用于core.async。
https://gist.github.com/ghadishayban/fe84eb8db78f23be68e20addf467c4d4
它使用显式堆栈来存放seqs/bindings。
它目前还没有处理任何分块或修饰符。

0
...