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

看一眼源代码,就可以明确地看出假设是正确的——内联版本仅适用于arity 2,其他则反映。

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

评论者:gfredericks

我可以尝试一下。

0

评论者:gfredericks

我认为没有更改编译器其他地方代码,解决反射警告问题几乎是不可能的,因为在 {{aget}} 中完成的反射与正常的clojure反射不同——它是明确在函数体中执行的,而不是由编译器生成的。由于编译器没有生成它,它合理地不知道它在那里。因此,即使 {{aget}} 对其他arity进行了修复,你仍然不会在未内联的情况下收到警告。

我可以想象某种类型的元数据,你可以把它放在函数上,告诉编译器它将在未内联时进行反射。或者可能是一个更通用的未内联警告?

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

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

我尝试了 (link: ~alexmiller) 提出的实现。该补丁使用 getComponentType 而不是字符串魔法,并与第一个补丁相比有了以下改进

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

缺点是,现在多参数调用 {{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(由 alex+import 报告)
...