请在 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:类文件 user$eval1032 中无效的方法代码长度 69883,编译器:(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
`

以下是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 投票
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 hundred b hundred c hundred])))
;; 2毫秒

(dotimes [_ 5]
(time (doseq2 [a hundred b hundred c hundred])))
;; 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 选项。

我们能否使用这个小工具包直接针对 clojure.jar 进行测试,而不是直接针对 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),增加一些微小的操作并没有显著改变结果。对于(mapstr a)也是如此

`
(range 10000000) => (mapstr [a])
core/doseq
"已过时间: 586.822389 毫秒"
"已过时间: 563.640203 毫秒"
"已过时间: 369.922975 毫秒"
"已过时间: 366.164601 毫秒"
"已过时间: 373.27327 毫秒"
doseq2
"已过时间: 419.704021 毫秒"
"已过时间: 371.065783 毫秒"
"已过时间: 358.779231 毫秒"
"已过时间: 363.874448 毫秒"
"已过时间: 368.059586 毫秒"

`

以及对于内部操作如(inca)

`

(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 毫秒"
`

我仍然看到基于reducer的doseq优于原始版本,除了小的序列

0 投票
by

评论者:gshayban

如下表单将无法与此补丁一起使用

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

因为宏gotec无法遍历函数范围。我所知的唯一类似代码是core.async/mapcat**,一个支持已标记为弃用的函数的私有函数。

0 投票
by

评论者:gshayban

我看到just added 'clojure.core/run!',它也有类似的限制

0 投票
by

评论由: richhickey发表

请考虑Ghadi的反馈,尤其是关于闭包的反馈。

0 投票
答者:

评论者:gshayban

由于控制流的数量,当前在 go 表单下 doseq 的扩展并不理想。状态机中 14 个状态与 loop/recur 下的 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 报告)
...