2024 年 Clojure 状况调查! 分享您的想法。

欢迎!有关如何使用该页面,请参阅 关于 页面以获取更多信息。

+5
Clojure
以下是行为记录。我不能确定是否正在执行反射,但是性能惩罚(大约 1300x)表明可能确实在执行。


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: 最终 GC 需要 3.5198021166354323 % 的运行时间
WARNING: 最终 GC 需要 29.172288684474303 % 的运行时间
评估次数:在 6 个样本的 10593 次调用中为 63558。
             执行时间平均值: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: 最终 GC 需要 40.59820310542545 % 的运行时间
评估次数:在 6 个样本的 10355906 次调用中为 62135436。
             执行时间平均值:6.999273 ns
    执行时间标准偏差:0.112703 ns
   执行时间下四分位数:6.817782 ns ( 2.5%)
   执行时间上四分位数:7.113845 ns (97.5%)
                   消耗的过度开销:2.477107 ns


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

16 答案

+2

评论者:ypeleg

Bump. 我刚刚因此遭受了打击。

这里有两个独立的问题。
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的变长函数调用内联。它假设如果它看到数组arg上的{{: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

评论者:alexyakushev

重新上传补丁:修复了微小的问题。

0

评论者:alexyakushev

顺便问一下,为什么关于{{aset}}的文档字符串说它只对引用类型数组有效?这条注释过时了吗?我们可以途中修复它。

0

评论者:alexyakushev

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

0

评论者:alexmiller

很复杂,不确定这是否值得。可能不会考虑用于1.10。

0

评论者:alexyakushev

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

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