2024年Clojure调查问卷!中分享您的想法。

欢迎!请参阅关于页面了解有关此如何工作的一些更多信息。

+9
Clojure

最近在进行性能工作时,我发现将代码展开为单个关联调用要比使用多个键快得多(对我的特定应用程序而言大约快10%)。然后,Zachary Tellman 指出,clojure.core 在键的数量相对较少的情况下根本不会展开关联。

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

我还没有为这个问题开发补丁,但我进行了一些独立的基准测试

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个元素的持久HashMap(未展开) | 190ns | 313ns | 551ns | 651ns |
| 20个元素的持久HashMap(展开关联) | N/A | 250ns | 433ns | 524ns |
| | | | | |
| 记录(未展开) | 12ns | 72ns | 105ns | 182ns |
| 记录(展开关联) | N/A | 21ns | 28ns | 41ns |

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

基准测试是在商品服务器上进行的(8个CPU,32gb内存),使用 ubuntu 12.04 和一个最新的Java 8版本。包含 cpuinfounamejava -version 会话输出。

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

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

关于这个修补程序有一个悬而未决的问题:我们应该展开这些调用多远?update(在1.7 alpha中展开)展开到3个参数。增加更多的展开并不困难,但这会影响关联的可读性。

补丁: CLJ-1656-v5.patch

25 个回答

0

评论者:tcrayford

好吧,附上了 assoc.diff,它将此递归展开到比当前代码多一个层级(因此支持非递归的两个键/值对)。如果我们继续沿着这条路走下去,代码将变得越来越复杂,所以我不确定这是否是一个好方法,但是性能提升似乎非常有说服力。

0

评论者:michaelblume

由于递归展开有些复杂,为什么不让我们用宏来自动生成呢?

0

评论者:michaelblume

补丁v2已包含 assoc!

0

评论者:tcrayford

我用相似的方式进行递归展开,对核心数据类型(列表、集合、向量,每一样都为空,然后又加入了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 |

基准测试是在之前的同一个机器上进行的。在这里,除了列表之外,其他的优势并不明显,列表中的创建序列和递归的开销似乎明显决定了真正进行联合操作的代价(这是有道理的 - 任何元素列表上的联合都应是一个非常便宜的操作)。原始基准输出在这里:https://gist.github.com/tcrayford/51a3cd24b8b0a8b7fd74

0

评论者:tcrayford

Michael Blume: 我非常喜欢这些补丁!它们读起来比我的原始补丁要好得多。你是否检查过这些宏生成的方法是否触发了Hotspot的hot代码内联限制?(限制是235字节码)。这是我在这里使用宏的唯一担忧 - 很容易生成代码来克服内联器。

0

评论者:michaelblume

谢谢!对我来说这是个新问题,所以我可能做了错事,但我只是运行了nodisassemble查看这两个定义,并查看每行旁边的“指令编号” tallest 的arity为varargs是多达219,球菌为251,所以,假设我看的是对的,也许应该减掉一个arity?如果我去掉最高的arity,varargs变成232,略低于标准。

或者,我们可以在varargsarity中调用assoc!而不是assoc!*,这将去掉很多代码 -- 在这种情况下,varargs为176,六个对为149。

0

评论者:michaelblume

哎呀,我忘记在关联 assoc的varargs调用中包含coll了

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

0

评论者:michaelblume

好的,这是我在观察了反汇编输出后做的几个修复。我对 assocKNOWNNAME宏做了更改,以确保它能够正确类型提示 -- 我真的不确定为什么它之前没有正确类型提示,但现在它确实如此。另外,我把 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)
(从 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)
0

评论者:michaelblume

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

0

评论者:michaelblume

现有的 assoc 版本会进行运行时检查以确保传递的 varargs 数量是偶数,而 assoc! 不会。我们是否想要保留这种行为或同时在两者中进行检查?

0

评论者:michaelblume

此外,我很想知道内联在这里的相关性 —— 当存在 getRootBinding 步骤时,HotSpot 内联确实会与 Var 调用一起工作吗?

...