请分享您的想法,参加 2024 年 Clojure 调查问卷!

欢迎!请查看 关于 页面,了解更多关于这个网站的工作原理。

0
Clojure

重要性能提示 新的实现在自定义 reducible 但未分块的情况下更快,并且对于大量绑定也更快。原始实现在分块集合上进行了手动优化,并且在分块较大而绑定较小的情况下获胜,可能是由于 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 类文件中的方法代码长度无效,编译:(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' 测试也同时是 '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 的补丁 doseq.patch(日期为 2014 年 6 月 25 日)后的结果

user=> (formsize '(doseq [x (range 10)] (print x))) 93

user=> (formsize '(doseq [x (range 10) y (range 10)] (print x y))) 170

0
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

`
很希望看到包含和不含此补丁的性能结果。
by
评论由:stu 提供
在下面的测试中,新的实现叫做 "doseq2",而原始的实现叫做 "doseq"。

(def hund (into [] (range 100)))
(def ten (into [] (range 10)))
(def arr (int-array 100))
(def s "superduper")

(def ten (into [] (range 10)))
;; 大序列,少量绑定:doseq2 失败
(dotimes [_ 5]

(time (doseq [a (range 100000000)])))
(def ten (into [] (range 10)))
;; 1.2 秒
(time (doseq2 [a (range 100000000)])))

(def ten (into [] (range 10)))
;; 1.8 秒
;; 小的未分段的可缩减序列,少量绑定:doseq2 胜出

(def ten (into [] (range 10)))
(time (doseq [a s b s c s])))
;; 0.5 秒

(def ten (into [] (range 10)))
(time (doseq2 [a s b s c s])))
;; 0.2 秒

(time (doseq [a arr b arr c arr])))
(def ten (into [] (range 10)))
;; 40 毫秒
(time (doseq2 [a arr b arr c arr])))

(def ten (into [] (range 10)))
时间(doseq2 [a hund b hund c hund])
;; 0.2 秒

;; 更多绑定:doseq2的性能不断提高
(def ten (into [] (range 10)))
时间(doseq [a ten b ten c ten d ten])
(time (doseq2 [a arr b arr c arr])))

(def ten (into [] (range 10)))
时间(doseq2 [a ten b ten c ten d ten])
;; 0.4 毫秒

(def ten (into [] (range 10)))
时间(doseq [a ten b ten c ten d ten e ten])
;; 18 毫秒

(def ten (into [] (range 10)))
时间(doseq2 [a ten b ten c ten d ten e ten])
;; 1 毫秒
`

0

评论由:gshayban 提供

嗯,我无法重现你的结果。

我不确定你是否用lein测试的,在什么平台上,以及什么jvm选项。

我们可以用这个小套件直接对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 提供

基准测试中包含空数列的身体,是为了隔离遍历的损耗,这是好事。然而,这仅占总代码的 0%。

至少对于第一次基准测试(大块数列),增加少量工作对结果没有显著改变。对于 (map str (link: a)) 同样如此

`
(range 10000000) => (map str [a])
核心/数列
"已过时间: 586.822389 毫秒"
"已过时间: 563.640203 毫秒"
"已过时间: 369.922975 毫秒"
"已过时间: 366.164601 毫秒"
"已过时间: 373.27327 毫秒"
数列2
"已过时间: 419.704021 毫秒"
"已过时间: 371.065783 毫秒"
"已过时间: 358.779231 毫秒"
"已过时间: 363.874448 毫秒"
"已过时间: 368.059586 毫秒"

`

也不是对于 Built-in like (inc a)

`

(range 10000000)
核心/数列
"已过时间: 317.091849 毫秒"
"已过时间: 272.360988 毫秒"
"已过时间: 215.501737 毫秒"
"已过时间: 206.639181 毫秒"
"已过时间: 206.883343 毫秒"
数列2
"已过时间: 241.475974 毫秒"
"已过时间: 193.154832 毫秒"
"已过时间: 198.757873 毫秒"
"已过时间: 197.803042 毫秒"
"已过时间: 200.603786 毫秒"
`

我仍然看到基于 reduce 的 doseq 早于原始版本,除了一小部分序列。

0
by

评论由:gshayban 提供

下面的表单无法与这个补丁一起工作

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

因为 go 宏无法遍历函数的界限。我知道的唯一这样的代码是 core.async/mapcat**,一个私有函数支持一个被标记为弃用的函数。

0
by

评论由:gshayban 提供

我看到了新的 #'clojure.core/run!',它有相似的限制

0
by

评论者:richhickey

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

0

评论由:gshayban 提供

由于控制流的数量,形式为 go 的 doseq 推广当前不足理想,状态机中的 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,因此适合核心异步。
https://gist.github.com/ghadishayban/fe84eb8db78f23be68e20addf467c4d4
它为 seqs/bindings 使用显式的堆栈。
它还没有处理任何分块或修饰符。

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