请分享您的想法,参与 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: Final GC required 3.5198021166354323 % of runtime
WARNING: Final GC required 29.172288684474303 % of runtime
评估次数: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: Final GC required 40.59820310542545 % of runtime
评估次数:62135436 在 6 个包含 10355906 次调用的样本中。
             执行时间均值:6.999273 ns
    执行时间标准差:0.112703 ns
   执行时间下四分位数:6.817782 ns ( 2.5%)
   执行时间上四分位数:7.113845 ns (97.5%)
                   过度使用:2.477107 ns


*原因:内联版本只适用于二元数,否则会进行反射。

16 个答案

+2

评论由:ypeleg 提出

跟进。我刚刚被这个问题坑惨了。

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

注意,当维数增多时,这会变得更糟,例如 (get doubles3d 0 0 0)。
这将慢1M,等等 - 并非如此,除非你迭代所有元素,它只是 foreach 查找。
简单的 n_dims**1000x 每次查找。

让人意外的缺点,尤其是考虑到你经常为了速度而使用原始数组,
并且一个常见的用例是有一个内部循环迭代数组。

0
by
_评论者:gfredericks_

看一眼源代码,就可以明显看出假设是正确的——内联版本只适用于二元arity,否则会反射。

我认为只要将内联函数改为变长参数(使用reduce)就可以了,但试着做了之后我明白了这很棘手,因为必须为每个步骤生成正确的类型提示。例如,给定 {{\^"[[D"}} 的内联函数需要将中间结果类型提示为 {{\^"[D"}}。如果我们只是处理以方括号开头的字符串,这并不困难,但我不能确定这是唯一可能性。
0
by

评论者:gfredericks

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

0
by

评论者:gfredericks

我认为要在不更改编译器其他地方代码的情况下,几乎不可能解决这个问题,因为 {{aget}} 中的反射是与常规 Clojure 反射不同类型——它是显式在函数体中,而不是由编译器产生的。由于编译器没有生成它,它合理地不知道它存在。所以即使 {{aget}} 对其他 arities 做了修正,如果它没有被内联,你仍然不会收到警告。

我可以想象一种在函数上放置某种元数据,告知编译器如果不内联则会反射。或许是一个更通用的未内联警告?

添加另一个编译器标志的全局作用域似乎与数组函数因反射而无法发出警告的问题的严重性相当平衡。

0

评论者:gfredericks

附上了CLJ-1289-p1.patch,该文件简单地将可变参数调用直接嵌入到aget中。它假设如果在数组参数中看到以{{[}}开头的{{:tag}}字符串,它可以假设从一次{{aget}}调用返回的值可以用相同的字符串标记,并在起始{{[}}后去除。

我不是一个jvm专家,但通过阅读了一些规范,我认为这是一个合理的假设。

0

评论者:alexmiller

我认为这可能确实是正确的,但更官方地询问这个问题的方法可能是通过获取数组类型并调用 Class.getComponentType()(而不是字符串杂凑)。

0

评论者:gfredericks

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

0

评论者:gfredericks

I see (-> s (Class/forName) (.getComponentType) (.getName)) does the same thing -- is that route preferred, or is there another one?

0

评论者:alexiflekov

我尝试按照(链接:~alexmiller)的推荐实现。这个补丁使用getComponentType代替字符串魔术,与第一个补丁相比引入以下改进:

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

然而,对{{aset}}的多参数调用现在触发了编译器的反射,而这原本是{{Array.set}}的功能。我想知道,是否应该对这些问题的边缘进行平滑处理,或者在这种情况下,反射是否应该更加明显(显示在编译器警告中)。

我还附上了验证示例的REPL日志(使用clj-java-decompiler)。将其转换为测试将非常好,但我还不知道如何做到。

顺便说一下,我不得不使用{{((var aget) ...)}}这种hack来强制调用非内联版本。我并不喜欢这样做,但我还没有想出更好的方法。另一种方法是将其分离成独立的私有函数,但我不想在核心命名空间中添加更多的变量。

0

评论者:alexiflekov

补丁已重新上传:轻微修复。

0

评论者:alexiflekov

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

0

评论者:alexiflekov

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

0

评论者:alexmiller

这有些复杂,不确定是否值得投入1.10。可能不会考虑它了。

0

评论者:alexiflekov

感谢您的回答。在复杂性的方面,我可以移除“优化”反射调用的部分,这既简化了算法,又使编译器(和用户)可以看到反射点。

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