2024Clojure状况调查! 中分享您的看法。

欢迎!有关如何运行的更多信息,请参阅关于 页面。

+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


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

16 回答

+2

评论者:ypeleg

提升。我刚刚为此问题成名。

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

注意,随着维度的增加,这种情况会变得更加严重,例如(get doubles3d 0 0 0)
-会慢1M,等等'这不是真的,除非你遍历所有元素。它只是
每次查找时n_dims**1000倍。

令人惊讶的情况,考虑到你经常为了速度而去访问原始数组,
并且一般的情况是在内部循环中遍历数组。

0
评论由:gfredericks_

查看源代码清楚地表明,这个假设是正确的——内联版本只适用于arity 2,否则会进行反射。

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

评论由:gfredericks

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

0

评论由:gfredericks

我认为没有修改编译器中其他部分的代码,几乎无法解决这个问题。因为{{aget}}中的反射与常规Clojure反射不同——它是显式地在函数体中,而不是由编译器生成的。由于编译器没有生成它,所以它不知道它的存在。因此,即使{{aget}}为其他arity进行了修复,你仍然无法在没有内联时收到警告。

我可以想象可以在函数上放置一些元数据,告诉编译器如果它没有被内联,就会进行反射。或者 maybe 一个更通用的not-inlined警告?

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

0
by

评论由:gfredericks

附加了CLJ-1289-p1.patch,它只是简单地内联了aget的变长调用。它假设如果它在数组参数的{{:tag}}上看到一个以{{[}}开始的字符串,它就可以假设从一次{{aget}}调用返回的值可以用相同字符串标记,并且将前导{{[}}去掉。

我不是jvm专家,但读了一小部分规范后,我认为这是一个合理的假设。

0
by

评论者:alexmiller

我认为这可能是真实的,但用更正式的方式提出问题的方法是将数组类与Class.getComponentType()一起使用(比字符串打杂更少janky)。

0
by

评论由:gfredericks

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

0
by

评论由:gfredericks

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

0
by

评论者:alex yakushev

我对(链接:~alexmiller)建议的实现进行了尝试。附加的补丁使用getComponentType代替了字符串魔术,并且与第一个补丁相比有以下改进:

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

从另一方面来看,对{{aset}}的多参数调用现在会触发编译器的反射,而之前是{{Array.set}}。我 wonder whether平缓这些边缘也有意义,或者在这种情况下更可见的反射(在编译器警告中显示)会更好。

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

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

0

评论者:alex yakushev

补丁重新上传:小修。

0

评论者:alex yakushev

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

0

评论者:alex yakushev

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

0

评论者:alexmiller

很复杂,不确定是否值得这么做。可能不会查看1.10了。

0

评论者:alex yakushev

谢谢你的回答。关于复杂性,我可以移除“优化”反射调用的部分,这既简化了算法,又使编译器(以及用户)能够看到反射点。

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