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))
警告:最终垃圾收集器占用3.5198021166354323 %的运行时间
警告:最终垃圾收集器占用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))
警告:最终垃圾收集器占用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的1000次方。

一个令人惊讶的问题,尤其是考虑到你经常为了速度而去原始数组,
一个常见的使用场景就是内部循环(们)遍历数组。

0
评论者:gfredericks

看一眼源代码就会很明显,假设是正确的——内联版本只适用于2个参数,其他情况则反映。

我想这很简单,只要把内联函数转换成变长参数(使用reduce),但尝试过后我发现这很棘手,因为你需要为每一步生成正确的类型提示。例如,给定{{\^"[[D"}},内联函数需要用{{\^"[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

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

0
by

评论由: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
已回答 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 报告)
...