欢迎!请参阅关于页面,了解更多关于该工具如何工作的信息。
在最近进行性能工作时,我发现将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的最新版本。附带的文件包括cpuinfo,uname和java -version输出。
启用了相对标准的JVM生产标志,并注意禁用了leiningen的启动时间优化(这禁用了许多JIT优化)。
可以通过克隆存储库并运行script/bench来运行基准测试。
关于这个补丁有一个悬而未决的问题:我们应该展开这些调用的程度有多深?1.7 Alpha中已展开的update是展开到3个参数。添加更多的展开并不难,但这会影响assoc的可读性。
补丁: CLJ-1656-v5.patch
评论者:tcrayford
好的,附件中包含了 assoc.diff,它将此展开到比当前代码多一个级别(因此支持不通过递归的两个键/值对)。如果我们继续这种方式,代码将会变得非常复杂,所以我不确定这是否是一个好的方法,但是性能提升似乎非常有吸引力。
assoc.diff
评论者:michaelblume
由于展开的结果有些复杂,为什么不让宏为我们编写它呢?
补丁 v2 包含了 assoc!
在相同的机器上对与类似展开的 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 |
在相同的机器上进行了基准测试。这里只有列表有一些不太明显的优势,除了列表,创建序列和递归的开销似乎明显占主导地位,这符合实际情况 - 任何元素列表上的 conj 应该是一个非常便宜的操作)。原始基准输出在这里:[https://gist.github.com/tcrayford/51a3cd24b8b0a8b7fd74](https://gist.github.com/tcrayford/51a3cd24b8b0a8b7fd74)
Michael Blume:我喜欢这些补丁!它们读起来比我原来的补丁好多了。你检查过这些宏生成的任何方法是否超出了Hotspot的热代码内联限制吗?(它是235个字节码)。这可能是我在这里使用宏的唯一担忧 - 很容易生成可以击败内联器的代码。
谢谢!这对我是新的,所以我可能做错了,但我只是运行了两个定义的 nodisassemble,并且每行的“指令编号”都增到了219,对于varargs的arity,增到了251,对于assoc!,所以,如果我确实看对了,可能那个需要一个arity减掉?如果我移除最高的arity,varargs变成了232,这正好低于分数线。
我想我们可以用assoc!代替varargs中的assoc*,这在某种程度上减少了很多代码--在这种情况下,varargs是176,六个对是149。
糟糕,我忘记在varargs中对assoc!调用包括coll了。
这让我想起这个补丁需要测试。
好吧,这有一些我在查看反汇编输出后做的修复。我对assoc!*宏进行了一些改动,以确保它类型提示正确--我 honestly不觉得之前为什么没有正确类型提示,但现在它确实如此。我还将varargs版本顶部六个条目的调用从宏调用更改为函数调用,以便它能够在251个可内联字节码内完成。(这也是基于我对输出正确性的假设)。
Michael:你想要把带有这些补丁的分支推送到clojars或者其他地方吗?这样我就可以用补丁中的确切代码重新运行基准测试了。
嗯,我不确定我如何去做这个操作,但是这里有一个在github上的分支 https://github.com/MichaelBlume/clojure/tree/unroll-assoc
v5版本将辅助宏标记为私有。
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)(从central检索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)
好的,你是如何构建的?为什么clojure group名这么奇怪?
现有的assoc版本在运行时会检查是否传递了偶数个可变参数,但assoc!并不这么做。我们想要保持这种行为还是同时在两者中检查?
此外,我想知道内联在这里的相关性如何--当有getRootBinding步骤时,HotSpot内联是否真的与Var调用一起工作?