2024 Clojure状态调查!中分享您的想法。

欢迎!请查看关于页面,了解有关其工作方式的更多信息。

+9
Clojure

最近在进行性能工作时,我发现将关联展开为单个关联调用比使用多个键快得多(我的特定应用程序为10%)。接着Zachary Tellman指出,clojure.core根本不会展开关联,即使是对于相对较低的键数量的情况。

我们已经展开了其他通过apply调用的性能关键函数,例如update,但(我认为在大量应用程序和库的临界路径上)关联会因此受益。

我尚未为这个功能开发补丁,但我进行了一些独立的基准测试工作

https://github.com/yeller/unrolling-assoc-benchmarks

基准结果

代码:https://github.com/yeller/unrolling-assoc-benchmarks/blob/master/src/bench_assoc_unrolling.clj

| |1 |2 |3 |4 |
| :-- | :-- | :-- | :-- | :-- |
| 空数组映射(未展开) | 23ns | 93ns | 156ns | 224ns |
| 空数组映射(展开assoc) | N/A | 51ns | 80ns | 110ns |
| | | | | |
| 20元素持久HashMap(未展开) | 190ns | 313ns | 551ns | 651ns |
| 20元素持久HashMap(展开assoc) | N/A | 250ns | 433ns | 524ns |
| | | | | |
| 记录(未展开) | 12ns | 72ns | 105ns | 182ns |
| 记录(展开assoc) | N/A | 21ns | 28ns | 41ns |

每项测量都在单独的JVM中进行的,以避免JIT路径依赖。

基准测试在商用服务器(8个cpu,32gb ram)上运行,使用ubuntu 12.04和Java 8的最新版本。附带了cpuinfounamejava -version输出来。

启用了相对标准的JVM生产标志,并且小心地禁用了leiningen的启动时间优化(这会禁用许多JIT优化)。

可以通过克隆存储库并运行script/bench来运行基准测试。

关于这个补丁有一个待解决的问题:我们应该展开多少调用?

补丁: CLJ-1656-v5.patch

25 个回答

0

评论由: tcrayford

好吧,附带的 assoc.diff,它将这个递归展开为比当前代码多一个级别的单级(因此支持不超过两个键值对)。如果我们继续这样下去,代码将变得非常复杂,所以我不确定这种方法是否合适,但是性能的提升似乎是很有吸引力的。

0

评论由: michaelblume

由于递归展开变得相当复杂,为什么不让宏来为我们编写它呢?

0

评论由: michaelblume

补丁v2包括了assoc!

0

评论由: tcrayford

我用相似的方式对寂合进行基准测试,涵盖了从核心(列表、集合、向量)到相当广泛的 datatypes,并分别用空和20个元素的每个进行测试

| | 1 | 2 | 3 | 4 |
| :-- | :-- | :-- | :-- | :-- | :-- |
| 空向量(未展开) | 19ns | 57ns | 114ns | 126ns |
| 空向量(展开寂合) | N/A | 44ns | 67ns | 91ns |
| | | | | |
| 20个元素的向量(未展开) | 27.35ns | 69ns | 111ns | 107ns |
| 20个元素的向量(展开寂合) | N/A | 54ns | 79ns | 104ns |
| | | | | |
| 空列表(未展开) | 7ns | 28ns | 53ns | 51ns |
| 空列表(展开寂合) | N/A | 15ns | 20ns | 26ns |
| | | | | |
| 20个元素的列表(未展开) | 8.9ns | 26ns | 49ns | 49ns |
| 20个元素的列表(展开) | N/A | 15ns | 19ns | 30ns |
| | | | | |
| 空集合(未展开) | 64ns | 170ns | 286ns | 290ns |
| 空集合(展开) | N/A | 154ns | 249ns | 350ns |
| | | | | |
| 20个元素的集合(未展开) | 33ns | 81ns | 132ns | 130ns |
| 20个元素的集合(展开) | N/A | 69ns | 108ns | 139ns |

基准测试在之前的同一台机器上运行。这里的优势不是很明显,除了列表,创建 seqs 和递归的开销似乎在 clearly dominating 实际执行 conj 的成本(这很合理——在任何元素列表上的 conj 应该是一个非常便宜的运算)。原始基准输出在这里:[https://gist.github.com/tcrayford/51a3cd24b8b0a8b7fd74](https://gist.github.com/tcrayford/51a3cd24b8b0a8b7fd74)

0

评论由: tcrayford

Michael Blume: 我喜欢这些补丁!它们对我的读起来比我的原始补丁要好得多。你检查了这些宏生成的方法是否使 Hotspot 的热代码内联限制?(限制为 235 字节码)。这是我在这里使用宏的唯一担忧——很容易生成会打败内联器的代码。

0

评论由: michaelblume

谢谢!这对我而言是新事物,所以我可能正在做错,但我只是运行了 nodisassemble 在两个定义上,并且在每行旁边的“指令编号”中,对于 varargs arity assoc 达到了 219,对于 assoc! 达到了 251,所以,假设我观察的是正确的内容,可能该有一个 arity 被移除?如果我去掉最高的 arity,varargs 将得到 232,这正是下面的界限。

相反,我们可以在 varargs arity 中调用 assoc! 而不是 assoc!*,这将删除大量代码——在这种情况下,varargs 为 176,六对为 149。

0

评论由: michaelblume

哎呀,我忘了在 varargs 的 assoc! 调用中包括 coll。

这让我想起,这个补丁需要测试。

0

评论由: michaelblume

OK,我在检查了反汇编输出后做了一些更改。对 assoc!* 宏的更改以确保它正确地进行类型提示——我 honestly 不确定为什么之前它没有正确地进行类型提示,但现在它确实如此。我还将 varargs 版本顶部的前六个条目的调用从宏调用更改为函数调用,以便它能够适应 251 个可内联的字节码。(这再次是基于我正确读取输出假设的情况下)。

0

评论由: tcrayford

迈克尔:想把这些补丁推送到clojars或者其他地方吗?然后我可以使用补丁中的确切代码重新运行基准测试。

0

评论由: michaelblume

嗯,我不确定我是否知道如何做这个——不过github上的这个分支可以这样操作https://github.com/MichaelBlume/clojure/tree/unroll-assoc

0

评论由: michaelblume

版本v5将辅助宏设置为私有。

0

评论由: tcrayford

迈克尔:那个分支是基于clojure/clojure master的吗?我尝试使用它运行基准测试,但在构建代码时遇到了未定义变量的错误(alpha5中不会发生这种情况)

(从clojars检索com/yellerapp/clojure-unrolled-assoc/1.7.0-unrollassoc-SNAPSHOT/clojure-unrolled-assoc-1.7.0-unrollassoc-20150213.092242-1.pom)
(从clojars检索com/yellerapp/clojure-unrolled-assoc/1.7.0-unrollassoc-SNAPSHOT/clojure-unrolled-assoc-1.7.0-unrollassoc-20150213.092242-1.jar)
(从中央检索org/clojure/clojure/1.3.0/clojure-1.3.0.jar)
线程 "main" 中发生异常:无法在当前上下文中解析符号:bench,在(bench_assoc_unrolling.clj:5)编译

at clojure.lang.Compiler.analyze(Compiler.java:6235)
at clojure.lang.Compiler.analyze(Compiler.java:6177)
at clojure.lang.Compiler$InvokeExpr.parse(Compiler.java:3452)
at clojure.lang.Compiler.analyzeSeq(Compiler.java:6411)
at clojure.lang.Compiler.analyze(Compiler.java:6216)
at clojure.lang.Compiler.analyze(Compiler.java:6177)
at clojure.lang.Compiler$BodyExpr$Parser.parse(Compiler.java:5572)
at clojure.lang.Compiler$FnMethod.parse(Compiler.java:5008)
0

评论由: michaelblume

好的,你是如何构建的?为什么clojure组这么奇怪?

0

评论由: michaelblume

现有的版本中,assoc执行运行时检查,以确保传入的varargs数量为偶数,而assoc!没有。我们希望保留这种行为的某种检查还是在两边都进行检查?

0

评论由: michaelblume

此外,我也很好奇内联在此处的相关性——当存在 getRootBinding 步骤时,HotSpot 的内联是否真的与 Var 调用兼容?

...