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

欢迎!请查阅关于页面以获取更多关于如何使用本站的信息。

+5
Clojure
以下是行为记录。我不知道是否进行了一些反射,但性能损失(约 1300 倍)表明可能是这样。


user=> (use 'criterium.core)
nil
user=> (def b (make-array Double/TYPE 1000 1000))
#'user/b
user=> (quick-bench (aget ^"[[D" b 304 175))
警告:最终 GC 需要 3.5198021166354323 % 的运行时间
警告:最终 GC 需要 29.172288684474303 % 的运行时间
评估计数:63558 在 6 个样本的 10593 次调用中。
             执行时间平均值:9.457308 µs
    执行时间标准偏差:126.220954 ns
   执行时间下四分位数:9.344450 µs(2.5%)
   执行时间上四分位数:9.629202 µs(97.5%)
                   使用的开销:2.477107 ns


一个解决方案是使用多个 aget。


user=> (quick-bench (aget ^"[D" (aget ^"[[D" b 304) 175))
警告:最终 GC 需要 40.59820310542545 % 的运行时间
评估计数:62135436 在 6 个样本的 10355906 次调用中。
             执行时间平均值:6.999273 ns
    执行时间标准偏差:0.112703 ns
   执行时间下四分位数:6.817782 ns(2.5%)
   执行时间上四分位数:7.113845 ns(97.5%)
                   使用的开销:2.477107 ns


*原因:内联版本仅适用于 arity 2,否则会进行反射。

16 个答案

+2

评论由:ypeleg 提出

增加。

这里有两个单独的问题
1) (aget 2d-array-doubles 0 0 ) 不发出反射警告。
2) 它似乎编译器有足够的信息来避免这里的反射调用。

-请注意,随着维度数量的增加,这种情况会越来越严重,例如(获取 doubles3d 0 0 0)-
-将会慢1M,等等'- 不一定,除非你遍历所有元素。实际上
每次查找都是n_dims**1000x。

这是一个令人惊讶的问题,尤其是考虑到你经常为了速度而使用原始数组,
并且一个常见的用例是内循环(们)遍历数组。

0
_评论人:gfredericks_

查看源代码会使我们明显地得出结论:该假设是正确的--内联版本只适用于2元,其他则反映了。

我认为这可以像将内联函数转换为可变参数(使用reduce)那样简单,但在尝试后我意识到这是困难的,因为你需要为每一步生成正确的类型提示。例如,给定 {{\^"[[D"}},内联函数需要在中间结果中带有类型提示 {{\^"[D"}}。如果我们只是处理以方括号开头的字符串,这并不难,但我不确定这是否是唯一可能性。
0

评论人:gfredericks

我可以试着解决这个问题。

0

评论人:gfredericks

我认为,除非更改编译器中的其他代码,否则很难解决这种反射警告问题,因为 {{aget}} 中的反射不同于正常的 clojure 反射--它是明确地在函数体中而不是由编译器发出的。由于编译器没有发出它,因此它也不知道它是否存在。因此,即使 {{aget}} 对其他参数进行了修复,如果您没有将其内联的话,您仍然不会收到警告。

我可以想象某种类型的元数据,您可以在函数上放置这种元数据来告诉编译器如果不在内联,则它将进行反射。或者,也许可以有更通用的非内联警告?

添加另一个编译器标志的全局作用域与数组函数无法警告反射的严重性相平衡。

0

评论人:gfredericks

附加了CLJ-1289-p1.patch,该补丁简单地内联了到aget的变异性调用。它假设如果它在数组参数上看到{{:tag}},并且它是一个以{{[}}开头的字符串,它就可以假设从一个调用到的{{aget}}返回的值可以用相同的字符串进行标记,并且去除了开头的{{[}}。

我不是JVM专家,但看了规范后,我认为这是一个合理的假设。

0

评论者:alexmiller

我认为这可能是真的,但一个更加官方的方式来提出这个问题就是获取数组类并调用Class.getComponentType()(比字符串魔术更稳固)。

0

评论人:gfredericks

根据{{:tag}}类型提示,您将如何获取数组类?

0

评论人:gfredericks

我看到了 (-> s (Class/forName) (.getComponentType) (.getName)) 做了同样的事情--这是首选方法吗,还是还有其他方法?

0

评论者:alexyakushev

我尝试了(链接: ~alexmiller)建议的实现。附加的补丁使用getComponentType代替字符串魔术,并且与第一个补丁相比有以下改进

  • 现在也支持aset。所有索引(除了最后一个)都展开成{{RT.aget}}调用,最外层的调用展开成{{RT.aset}}。
  • 如果内联版本在任何时候都无法解析数组类型,那么,而不是展开成N个{{RT.aget}}调用(这将导致N个编译器反射站点),它将生成对非内联{{aget}}调用的调用,该调用将使用{{Array.get}}(这是更快的)。

缺点是,现在对{{aset}}的多态调用会在编译器反射中触发,而之前它是{{Array.set}}。我怀疑是否最好让这些角落更加平滑,或者是否在这种情况下更明显的反射是更好的(它会在编译器警告中显示)。

我还附加了验证示例的REPL日志(使用clj-java-decompiler)。它将非常棒地转换成测试,但我还不知道如何做。

顺便说一下,我不得不使用一个重组 {{((var aget) ...)}} 来强制调用非内联版本。我不太喜欢这样做,但还没想出更好的方法。另一种方法是将其单独的非内联版本作为独立私用函数分割开来,但我不想向核心命名空间添加更多的变量。

0
by

评论者:alexyakushev

补丁重新上传:小幅度修正。

0
by

评论者:alexyakushev

顺便问一下,为什么 {{aset}} 的文档字符串说它只能用于引用类型的数组?这个注释是不是过时了?我们可以在这个过程中修复它。

0
by

评论者:alexyakushev

有机会在1.10中审查这个吗?也许实现得太复杂,需要更多时间考虑?我可能会尝试减少范围。

0
by

评论者:alexmiller

这很复杂,不确定是否值得这样做。可能不会在1.10中查看。

0
by

评论者:alexyakushev

谢谢你的回答。在复杂性的问题上,我可以移除“优化”反射调用部分的代码,这既简化了算法,也使得反射点对编译器(从而对用户)可见。

0
by
参考: https://clojure.atlassian.net/browse/CLJ-1289(由 alex+import 报告)
...