2024 Clojure 状况调查问卷 中分享您的想法!

欢迎!请参阅 关于 页面以了解更多关于这是如何工作的信息。

+5
Clojure
以下是行为记录。我不知道是否确实进行了反射,但性能惩罚(大约 1300x)暗示了这一点。


用户=> (use 'criterium.core)
nil
用户=> (def b (make-array Double/TYPE 1000 1000))
#'user/b
用户=> (quick-bench (aget ^"[[D" b 304 175))
警告:最终 GC 需要了运行时的 3.5198021166354323 %
警告:最终 GC 需要了运行时的 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。


用户=> (quick-bench (aget ^"[D" (aget ^"[[D" b 304) 175))
警告:最终 GC 需要了运行时的 40.59820310542545%
评估次数:62135436 在 6 个样本内的 10355906 次调用。
             执行时间平均值:6.999273 ns
    执行时间标准差:0.112703 ns
   执行时间下四分位数:6.817782 ns ( 2.5%)
   执行时间上四分位数:7.113845 ns (97.5%)
                   使用的开销:2.477107 ns


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

16 个答案

+2

评论者:ypeleg

更新。我正好被这个问题困扰了。

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

-注意,随着维度数的增长,这个问题会变得明显,即 (get doubles3d 0 0 0)-
不会慢1M,除非你遍历所有元素。它是
每次查找的n_dims**1000x。

令人惊讶的是,特别是考虑到你经常为了速度而去使用原始数组,
并且一个常见的用例是包含遍历数组的内循环。

0
评论者:gfredericks

查看源代码后,可以明显看出假设是正确的——内联版本只适用于二元,其他则反映。

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

评论者:gfredericks

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

0

评论者:gfredericks

我认为,在没有更改编译器中其他地方代码的情况下,几乎不可能解决反射警告问题,因为我们编译器中的{{aget}}所做的反射与正常的clojure反射不同——它明确地位于函数体中,而不是由编译器发出。因为编译器没有发出它,所以它根本不知道它的存在。所以即使{{aget}}对其他阶数进行了修复,当它不是内联的时候,你仍然不会得到警告。

我可以想象有为函数添加一些元数据的一种方式,告诉编译器如果它没有内联,它将会反射。或者也许是一个更通用的没有内联警告?

添加另一个编译器标志的全局范围似乎平衡了数组函数不能在反射上发出警告的严重性。

0

评论者:gfredericks

附件CLJ-1289-p1.patch,它简化了 variadic 调用的 aget。它假设如果它看到数组参数的 {{:tag}} 是以 {{[}} 开头的字符串,则可以假设从 {{aget}} 的一次调用得到的返回值可以用相同的字符串标记,并已剥离开头的 {{[}}。

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

0

评论由:alexmiller 撰写

我认为这可能实际上是真实的,但提出这个问题的更正式的方式是获取数组类并调用 Class.getComponentType() (而不是字符串篡改的方式)。

0

评论者:gfredericks

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

0

评论者:gfredericks

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

0

评论由: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(由 alex+import 报告)
...