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) 它似乎有足够的信息来避免在这里进行反射调用。

-注意,当维数增加时,这个问题会变得更加严重,例如(get doubles3d 0 0 0)-
-会慢1M,等等'- 这不正确,除非你遍历所有元素。它是
每个查找只需要n_dims**1000x。

这是一个令人讨厌的惊喜,尤其是考虑到你经常为了速度而使用原始数组,
并且一个常见的用例是内部循环(s)迭代数组。

0
_评论者:gfredericks_

看一眼源代码,可以清楚地看出这个假设是正确的 -- 内联版本仅适用于arity 2,其他情况下都会进行反射。

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

评论者:gfredericks

我可能可以尝试一下。

0

评论者:gfredericks

我认为除非改变编译器中的其他代码,否则几乎不可能解决反射警告问题,因为{{aget}}中进行的反射与正常clojure反射不同 -- 它明确位于函数体内,而不是由编译器发出。由于编译器没有发出它,所以它并不合理地知道它在那里。因此,即使{{aget}}针对其他arity进行了修复,除非它被内联,否则你仍然不会得到警告。

我可以想象一种可以在函数上放置的某种元数据,让编译器知道如果您不内联,它将进行反射。或者也许是一个更通用的非内联警告?

添加另一个编译器标志的全局范围似乎与数组函数不能对反射发出警告的严重性相平衡。

0

评论者:gfredericks

附加了CLJ-1289-p1.patch,该补丁简单地将变长调用inline到aget中。它假设如果它看到一个冒号开头为{{[}}的字符串标签在数组参数中,它可以假设从一次{{aget}}调用返回的值可以带有相同的字符串标签,并且首尾的{{[}}已被剥离。

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

0

评论人:alexmiller

我认为这可能是真的,但更正式提出这个问题的方法是获取数组的类,并调用Class.getComponentType()(而不需要字符串魔术操作)。

0

评论者:gfredericks

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

0

评论者:gfredericks

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

0

评论人:alexnakushev

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

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

缺点是,现在多参数调用{{aset}}会在编译器中触发反射,而之前是由{{Array.set}}触发的。我想知道是否应该平滑处理这些角落问题,或者是否在这种情况下使反射更明显(显示在编译器警告中)。

我还附加了验证例子的REPL日志(使用clj-java-decompiler)。将它们转换为测试会很好,但我还不知道如何操作。

顺便说一下,我不得不使用一种Hack {{((var aget) ...)}} 来强制调用非内联版本。不太好接受,但我还没有想出更好的办法。另一种方法是将其分开为独立的私有函数,但是我不想在核心命名空间中添加更多变量。

0
by

评论人:alexnakushev

补丁重新上传:小修复。

0
by

评论人:alexnakushev

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

0
by

评论人:alexnakushev

能否为1.10版本审查这个?也许实现太复杂,需要更多时间考虑?我可能会尝试缩小范围。

0
by

评论人:alexmiller

很复杂,不知道这样是否值得。可能不会为1.10版本检查它。

0
by

评论人:alexnakushev

谢谢你的回答。关于复杂性,我可以移除“优化”反射调用的部分,这将简化算法并使反射站点对编译器(和用户)可见。

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