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

欢迎!请参见 关于 页面以了解有关如何使用本网站的更多信息。

+5
Clojure
以下是行为记录。我不能确定是否正在执行反射,但是性能惩罚(大约1300x)暗示了这一点。


user=> (use 'criterium.core)
nil
user=> (def b (make-array Double/TYPE 1000 1000))
#'user/b
user=> (quick-bench (aget ^"[[D" b 304 175))
WARNING: Final GC required 3.5198021166354323 % of runtime
WARNING: Final GC required 29.172288684474303 % of runtime
评估计数:在6个包含10593次调用的样本中总共63558次。
             执行时间平均值: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))
WARNING: Final GC required 40.59820310542545 % of runtime
评估计数:在6个包含10355906次调用的样本中总共62135436次。
             执行时间平均值:6.999273 ns
    执行时间标准差:0.112703 ns
   执行时间低端值:6.817782 ns ( 2.5%)
   执行时间高端值:7.113845 ns (97.5%)
                   使用开销:2.477107 ns


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

16 个回答

+2

评论者:ypeleg

Bump. 我刚为此问题付出代价。

这里有两大问题
1) (aget 2d-array-doubles 0 0 ) 不产生反射警告。
2) 它 看起来 编译器有足够的信息来避免这里的反射调用。

-注意,这会随着维数的增加而变得越发严重,即 (get doubles3d 0 0 0)-
-将会慢1M倍,等等'- 除非你遍历所有元素。它不
每次查找只需 n_dims 的 1000 次运算。

这是一个令人意外的发现,特别是在考虑到你经常为了速度去使用原生态数组的情况下,
并且一个常见的使用场景是一个内部循环遍历数组。

0
评论者:gfredericks

看一下源代码,就可以明显看出这个假设是正确的 —— 内联版本仅适用于二元运算符,否则它会反映。

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

评论者:gfredericks

我可能可以尝试解决这个问题。

0

评论者:gfredericks

我认为要解决反射警告问题,基本上是不可避免的,除非在编译器的其他地方更改代码,因为 {{aget}} 中执行的反射与正常 clojure 反射不同 —— 这是显式在函数体中而不是由编译器生成的。由于编译器没有生成它,所以它合理地不知道它的存在。因此,即使 {{aget}} 对其他算子进行了修复,但您仍然无法在它没有内联时收到警告。

我可以想象一种某种类型的元数据,可以在函数上添加,告诉编译器它将反射(如果未内联)。或者也许是一个更通用的非内联警告?

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

0

评论者:gfredericks

附上 CLJ-1289-p1.patch,该补丁简单地将 aget 的可变参数调用内联。它假设,如果它看到了一个以 {{[}} 开始的字符串标签的数组参数,它可以根据从 {{aget}} 的一次调用返回的值将相同的字符串作为带有前导 {{[}} 的相同的字符串进行标记。

我并非 JVM 专家,但阅读了规范后,我认为这是一个合理的假设。

0

评论者:alexmiller

我认为这可能确实是正确的,但提出这个问题的正式方式是获取数组类并调用 Class.getComponentType()(而不是字符串魔术),这比字符串处理要干净利落。

0

评论者:gfredericks

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

0

评论者:gfredericks

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

0

评论者:alexyakushev

我尝试了 (链接: ~alexmiller) 建议的实现。这个附加的补丁使用 getComponentType 而不是字符串魔法,并且与第一个补丁相比引入了以下改进

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

缺点是,现在多参数的 set 调用在编译器反射中触发,而以前这是 Array.set。我怀疑是否有必要对这些角落进行平滑处理,或者是否实际上更偏好在这些情况下反映更明显(显示在编译器警告中)。

我还附加了验证示例的 REPL 日志(使用 clj-java-decompiler)。把它转换成测试会很好,但我还不知道如何做到这一点。

顺便说一下,我不得不使用 {{((var aget) ...)}} 恶搞来强制调用非内联版本。我并不喜欢它,但我还没有想出什么更好的方法。另一种方式是将非内联版本分离成一个单独的私有函数,但我不想在核心命名空间中添加更多的 Vars。

0

评论者:alexyakushev

补丁重新上传:小修复。

0

评论者:alexyakushev

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

0

评论者:alexyakushev

有没有可能为1.10审查这个问题?或许实现太过复杂,需要更多时间考虑?我可能会尝试缩小范围。

0

评论者:alexmiller

挺复杂的,不确定是否值得。可能不会在1.10版本中查看。

0

评论者:alexyakushev

谢谢您的回答。在复杂性方面,我可以移除“优化”反射调用的部分,这既简化了算法,也让反射点可见于编译器(以及用户)。

0
参考:[https://clojure.atlassian.net/browse/CLJ-1289](https://clojure.atlassian.net/browse/CLJ-1289)(由 alex+import 提交)
...