欢迎!请参阅关于页面,了解有关此如何工作的更多信息。
最近在进行性能工作时,我发现将关联展开成单个调用要比使用多个键要快得多(在我的特定应用程序中约为10%)。然后扎卡里·特尔曼指出,clojure.core根本不会展开关联,即使是相对较少的键数量。
我们已展开其他通过 apply 调用的性能关键函数,例如 update https://github.com/clojure/clojure/blob/master/src/clj/clojure/core.clj#L5914,但是(我认为在许多应用程序和库的临界路径上),关联可能从中受益。
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 || 空数组映射(展开关联) | N/A | 51ns | 80ns | 110ns | | | | | | || 20个元素的持久性哈希表(未展开) | 190ns | 313ns | 551ns | 651ns || 20个元素的持久性哈希表(展开关联) | N/A | 250ns | 433ns | 524ns | | | | | | || 记录(未展开) | 12ns | 72ns | 105ns | 182ns || 记录(展开关联) | N/A | 21ns | 28ns | 41ns |
每个测量都是在单独的 JVM 中进行的,以避免 JIT 路径依赖性。
基准在商品服务器上运行(8个 CPU,32gb ram),使用 ubuntu 12.04 和最新的 Java 8 版本。附带了cpuinfo、uname和java -version的输出。
cpuinfo
uname
java -version
启用了相对标准的 JVM 生产标志,并注意禁用了 leiningen 的启动时间优化(这将禁用许多 JIT 优化)。
可以通过克隆存储库并运行 script/bench 来运行基准测试。
script/bench
关于此补丁有一个悬而未决的问题:我们应该将调用展开得多远?update(在 1.7 的 alpha 版本中展开)展开到 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 |
基准测试是在之前相同的机器上进行的。这里除了列表以外的收益不是很明显,对于列表来说,创建 seqs 和递归的开销似乎占主导地位,明显超过了实际上执行 conj 的成本(这很有道理——对任意元素列表的 conj 应该是一个非常便宜的运算)。原始基准测试输出在这里:[https://gist.github.com/tcrayford/51a3cd24b8b0a8b7fd74](https://gist.github.com/tcrayford/51a3cd24b8b0a8b7fd74)
迈克尔·布卢姆:我喜欢这些补丁!它们对我来说比原来的补丁读起来要好得多。你检查过那些宏生成的任何方法是否超出了Hotspot的hot代码内联限制吗?(237个字节码)。这就是我在这里使用宏的唯一担忧 —— 容易生成可以击败内联器的代码。
谢谢!这件事对我来说很新鲜,所以我可能做错了,但我刚刚尝试了nodisassemble两个定义,每行旁边的“指令号”分别高达229个和一个变长参数的关联性和高达251个关联!所以,假设我在看对的东西,可能需要减少一个参数?如果我去掉最高的参数,变长参数就会是232,这刚好低于限制。
我想,另一种可能是,我们可以将在变长参数中调用assoc!而不是assoc!*,这将删除大量的代码 —— 在这种情况下,变长参数为176,而六个成对为149。
哎呀,我忘记在变长参数调用assoc!中包含coll了。
这让我想起这个补丁需要测试。
好的,这是我对反汇编输出进行检查后做的修复。我修改了assoc!*宏,以确保它正确进行类型提示 —— 我真的很不清楚为什么之前没有正确类型提示,但现在它确实如此。此外,我将变长版本中顶部六个条目的调用从宏调用更改为函数调用,以便它能在251个可内联的字节码内完成。(这又假设我读输出是对的)。
迈克尔:你想把这些补丁推送到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)主线程 "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)
好的,你是怎么构建的?为什么clojure组这么奇怪?
现有的assoc版本在运行时检查传入的varargs数量是否为偶数,但assoc!不检查。我们是否想保留此行为或在两者中进行检查?
此外,我很好奇内联在这里的相关性是多少--在有getRootBinding步骤的情况下,HotSpot内联实际上是否与Var调用一起工作?