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生产标志,并注意禁用了leinigen的启动时间优化(这将禁用许多JIT优化)。

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

对于这个补丁有一个悬而未决的问题:我们应该如何展开这些调用?在1.7的alpha版本中已展开的update展开到了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 |

在之前的机器上运行了基准测试。这里除了列表之外,益处不太明显,因为创建序列和递归的开销似乎明显超过了真正执行合取(conj)的成本(这是合理的——任何元素列表上的合取应该是一个非常廉价的操作)。原始基准输出在这里: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

好吧,我对分析后的输出做了一些修正。我将assoc!*宏的改变调整为确保它正确地类型提示——说实话,我确实不确定为什么它之前没有正确类型提示,但现在它做到了。我还将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)
主线程中发生异常 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

此外,我很想知道内联的相关性在此处如何——在存在一个获取根绑定步骤时,HotSpot内联是否真的与Var调用一起工作?

...