请分享您的想法,参与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))
WARNING: 最终GC消耗了运行时的3.5198021166354323 %
WARNING: 最终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))
WARNING: 最终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


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

16 答案

+2

评论:ypeleg

补充。我刚被这个问题害惨了。

这里有两个独立的问题
1) (aget 2d-array-doubles 0 0) 不会发出反射警告。
2) 从编译器获得的信息看起来足以避免在这里执行反射调用。

请注意,当维度数量增加时,这种情况会变得更加明显,例如 (get doubles3d 0 0 0)
-将会慢1兆倍,等等'- 这不是真的,除非你迭代所有元素。它只是
n_dims**1000x每次查找。

这是一个令人不快的惊喜,尤其是在你经常为了速度而使用原始数组的时候,
并且一个常见的用例是内层循环(s)遍历数组。

0
_评论者:gfredericks_

查看源代码可以清楚地看到,假设是正确的——内联版本仅适用于2元,否则会进行反射。

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

评论者:gfredericks

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

0

评论者:gfredericks

我认为在没有更改编译器其他地方代码的情况下,基本上无法解决反射警告问题,因为 {{aget}} 中的反射与正常 Clojure 反射不同——它是显式在函数体中而不是由编译器发出的。由于编译器没有发出它,因此它即使合理也不知道它在哪里。因此,即使 {{aget}} 对其他多元数进行了修复,你仍然不会在它未内联时收到警告。

我可以想象一种某种类型的元数据,你可以将其放在函数上,以告诉编译器即使未内联它也会进行反射。或者可能是一个更通用的未内联警告?

添加另一个编译器标志的全局范围似乎与数组函数无法在反射上发出警告的严重程度相平衡。

0

评论者:gfredericks

附加的 CLJ-1289-p1.patch 仅将各种调用内联到 aget 中。它假设如果它看到一个以 {{[}} 开头的字符串 {{:tag}} 出现在数组参数中,它就可以假设从一次 aget 调用返回的值可以用相同的字符串标记,去除前导的 {{[}}。

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

0
by

评论者:alexmiller

我认为这可能是真实的,但一个更正式的方式去提问应该是获取数组类并调用 Class.getComponentType()(比字符串操作更规范)。

0
by

评论者:gfredericks

基于 {{:tag}} 类型提示,你如何获取数组类?

0
by

评论者:gfredericks

我看到 (-> s (Class/forName) (.getComponentType) (.getName)) 做同样的工作——这是首选的方法吗?或者还有其他方法?

0
by

评论者:alexyakushev

我尝试了 (link: ~alexmiller) 提议的实现。附加的补丁使用 getComponentType 而不是字符串魔法,与第一个补丁相比,以下是一些改进:

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

缺点是,现在调用多参数的 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 报告)
...