请在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


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

16 答案

+2

评论者:ypeleg

更新。我刚被这个问题痛击了。

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

-注意,随着维度数量的增加,这会越来越差,即(get doubles3d 0 0 0)
-将会慢1M,等等'- 除非你迭代所有元素。它是
简单地 n_dims 的 1000 倍/每次查找。

一个令人惊讶的意外,尤其是在你经常为了速度而转到原始数组的情况下,
以及一个常见的用例是内部循环(s)遍历数组。

0
_评论者:gfredericks_

查看源代码会使假设变得显而易见——内联版本只适用于二元,否则它反映了。

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

评论者:gfredericks

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

0

评论者:gfredericks

我认为在不更改编译器中其他地方的代码的情况下,基本上无法解决反射警告问题,因为 {{aget}} 中的反射与正常的 Clojure 反射不同——它是显式地放在函数体中,而不是由编译器发出。由于编译器没有发出它,因此它合理地不知道它在那里。因此,即使 {{aget}} 对于其他参数数进行了修复,你仍然不会在没有内联的情况下收到警告。

我可以想象一种某种类型的元数据,你可以将其放在函数上,告诉编译器如果不在内联中,就会进行反射。或者可能是一个更通用的非内联警告?

根据数组函数无法在反射上发出警告的严重性,添加另一个编译器标志的全局作用域看起来是平衡的。

0

评论者:gfredericks

附上了CLJ-1289-p1.patch,它简单地将对aget的变长调用内联。它假定如果它看到一个以{{[}}开始的字符串作为数组arg的{{:tag}},它可以假设从一次到{{aget}}的调用返回值可以用相同的字符串进行标记,同时移除开头的{{[}}。

我不是JVM专家,但稍微阅读了规范后,我认为这是一个合理的假设。

0
by

评论者:alexmiller

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

0
by

评论者:gfredericks

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

0
by

评论者:gfredericks

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

0
by

评论者:alexyakushev

我尝试实现(链接: ~alexmiller)建议的方法。附带的补丁使用getComponentType而不是字符串魔法,并且与第一个补丁相比有如下改进

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

缺点是,{{aset}}的多参数调用现在在编译器中触发反射,而不是像以前那样使用{{Array.set}}。我想知道是否有必要平滑这些角落,或者在这种情况下是否更有利于反映(在编译器警告中显示)。

我还附上了验证示例的REPL日志(使用cljjava-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 (由 alex+import 报告)
...