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

欢迎!请访问关于页面了解更多关于这样工作的信息。

+9
Clojure

最近在进行性能工作时,我发现将assoc展开为单个调用比使用多个键显著更快(我的特定应用程序大约提高了10%)。Zachary Tellman随后指出,clojure.core根本不会为相对较少的键展开assoc。

我们已将其他通过apply调用的性能关键函数展开,例如update https://github.com/clojure/clojure/blob/master/src/clj/clojure/core.clj#L5914,但我觉得在许多应用程序和库的关键路径上,assoc(我认为它会受益于此)可能会受益于此。

我还没有为此开发补丁,但我进行了一些独立基准测试

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个元素的持久哈希表(未展开) | 190ns | 313ns | 551ns | 651ns |
| 20个元素的持久哈希表(展开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来运行基准测试。

对此补丁有一个悬而未决的问题:我们应该将这些调用展开多远?update(它在1.7 alpha中进行展开)展开为3个参数。增加更多展开并不困难,但这会降低assoc的可读性。

修补程序:CLJ-1656-v5.patch

25 答案

0

评论者:tcrayford

好吧,附上了《assoc.diff》文件,将这段代码展开到比当前代码多一个层级(因此支持两个键值对,而不需要递归)。如果我们继续这种方式,代码将会变得非常复杂,所以我不确定这是否是一个好的方法,但是性能好处似乎非常有吸引力。

0

评论者:michaelblume

由于展开后变得相当复杂,为什么不让我们有宏来代写呢?

0

评论者:michaelblume

补丁v2包括assoc!

0

评论者:tcrayford

我对 conj 进行了基准测试,具有相似的展开,涵盖了从核心(列表、集合、向量,每个都为空然后再次添加20个元素)相对广泛的基类型。

| | 1 | 2 | 3 | 4 |
| :-- | :-- | :-- | :-- | :-- | :-- |
| 空向量(未展开) | 19ns | 57ns | 114ns | 126ns |
| 空向量(展开 conj) | N/A | 44ns | 67ns | 91ns |
| | | | | |
| 20个元素的向量(未展开) | 27.35ns | 69ns | 111ns | 107ns |
| 20个元素的向量(展开 conj) | N/A | 54ns | 79ns | 104ns |
| | | | | |
| 空列表(未展开) | 7ns | 28ns | 53ns | 51ns |
| 空列表(展开 conj) | 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 |

基准测试在之前的相同机器上进行。这里的好处不是很明显,除了列表,似乎创建序列和递归的开销明显超过了实际执行并发操作的代价(这一点是有道理的——对于任何元素列表的并发操作都应该是非常便宜的操作)。原始基准输出在此:https://gist.github.com/tcrayford/51a3cd24b8b0a8b7fd74

0

评论者:tcrayford

Michael Blume:我喜欢这些补丁!与我的原始补丁相比,它们的可读性要好得多。你检查过这些宏生成的方法是否超出了Hotspot的热代码内联限制吗?(它是235个字节码)。这可能是我在这里使用宏的唯一顾虑——很容易生成会被内联程序打败的代码。

0

评论者:michaelblume

谢谢!这对于我来说是个新问题,我可能做错了,但我刚刚运行了nodisassemble来检查这两个定义,并且每行的“指令编号”旁边上升到219,对于变长参数函数的关联函数是219,对于adj函数是251,所以,假设我没看错,可能需要去掉一个参数。如果我去掉最高的参数,得到变长参数函数是232,接近行数。

我想我们可以用关联函数代替变长参数函数中的关联函数*,这样可以移除很多代码——在这种情况下,变长函数是176,六个参数对是149。

0

评论者:michaelblume

哎呀,我忘了在变长参数函数调用关联函数时包括coll。

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

0

评论者:michaelblume

好的,这是我在分析了反汇编输出后做的些修改。我修改了关联函数*宏以确保它正确提示类型——我真心不知道为什么之前它没有正确提示类型,但现在它确实如此。我还将varargs版本开头六个条目的调用从宏调用更改为函数调用,以便它能够适应251个可内联的字节码。(这再次假设我正确读取了输出)。

0

评论者:tcrayford

Michael:是否要将带有这些补丁的分支推送到clojars或其他地方?这样我就可以使用补丁中的确切代码重新运行基准测试。

0

评论者:michaelblume

嗯,我不确定我是否知道如何做这件事——不过这里有一个在github上的分支 https://github.com/MichaelBlume/clojure/tree/unroll-assoc

0

评论者:michaelblume

v5版本将辅助宏标记为私有。

0

评论者:tcrayford

Michael:那个分支是不是基于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
主线程java.lang.RuntimeException:无法在此上下文中解析符号: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 group如此奇怪?

0

评论者:michaelblume

现有的assoc版本在运行时会检查传递的参数数量是否为偶数,但assoc!则不会。我们是否要保留这种行为,或者在两者中都进行检查?

0

评论者:michaelblume

另外,我也很好奇内联在此的相关性——当存在getRootBinding步骤时,HotSpot在内联Var引用上是否真正起作用?

...