请在 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 微秒
    执行时间标准差:126.220954 纳秒
   执行时间下四分位数:9.344450 微秒(2.5%)
   执行时间上四分位数:9.629202 微秒(97.5%)
                   使用开销:2.477107 纳秒


一种解决方案是使用多个 agets。


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


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

16 答案

+2

评论者:ypeleg

更新。我刚被这个问题困扰。

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

- 注意这会随着维数的增加而变得更为严重,例如 (get doubles3d 0 0 0)-
-将会慢1M,等等。”-事实并非如此,除非你迭代所有元素。实际上是
每次查找都简单地将n_dims**1000x相乘。

这可是个令人震惊的发现,尤其是考虑到你常常为了速度而转向原始数组,
并且一个常见的用例是内部循环(s)遍历数组。

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

评论者:alexyakushev

重新上传补丁:微调。

0

评论者:alexyakushev

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

0

评论者:alexyakushev

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

0

评论者:alexmiller

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

0

评论者:alexyakushev

谢谢您的回答。关于复杂性,我可以移除"优化"反射调用的部分,这既简化了算法,又使反射站点对编译器(以及用户)可见。

0
参考: https://clojure.atlassian.net/browse/CLJ-1289(由alexisport提交)
...