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 次调用的 6 个样本中。
             平均执行时间: 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 个样本中。
             平均执行时间: 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**1000x。

这是个令人惊讶的问题,特别是当你通常为了速度而使用原始数组的时候,
并且一个常见的使用场景是内部循环遍历数组。

0投票
_评论由:gfredericks_ 发表

查看源代码可以看出,这个假设是正确的——内联回调版本仅适用于arity 2,否则则反映。

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

评论由:gfredericks

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

0投票

评论由:gfredericks

我认为没有改变编译器其他地方的代码,反射警告问题几乎无法解决,因为在aget中进行的反射与正常clojure反射不同——它明确在函数体中而非由编译器产生。由于编译器没有产生它,因此它不知道它的存在。所以即使对于其他arity的aget进行了修复,你仍然在不内联时不会得到警告。

我可以想象一种在函数上添加的某种类型的元数据,可以告诉编译器它如果不内联则会反射。或者可能是一个更通用的非内联警告?

由于数组的函数无法警告反射,增加另一个编译器标志的全局作用域与问题的严重程度相平衡。

0投票

评论由:gfredericks

附带CLJ-1289-p1.patch,它简化地内联调用get。它假定如果它在数组参数上看到{{:tag}},并且是以{{[}}开始的字符串,它可以假定从一次执行get获得的返回值可以用同样的字符串标记,首字符{{[}}被去除。

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

0投票
by

评论者:alexmiller

我认为这可能是真的,但更正式的方式是获取数组类并调用Class.getComponentType()(比字符串操作更简单)来询问问题。

0投票
by

评论由:gfredericks

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

0投票
已回答 by

评论由:gfredericks

我看(将符号( -> s (Class/forName) (取组件类型的).取名称)与(链接: ~alexmiller)做同样的工作。这是首选方法,还是还有其他方法?

0投票
by jira

评论者:alexmill谢

我已经试手感试根据(链接: ~alexmiller)。附带的补丁使用getComponentType代替字符串魔法,并且与第一个补丁相比引入了以下改进:

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

缺点是,现在对{{aset}}的多参数调用会在之前是{{Array.set}}的地方触发编译器的反射。我想知道是否应该平滑这些角落,或者是否最好在这种情况下的反射更明显(显示在编译器警告中)。

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

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

0投票

评论者:alexmill谢

重新上传补丁:小的修复。

0投票

评论者:alexmill谢

BTW,为什么{{aset}}的文档字符串说它只适用于引用类型的数组?这个注释过时了吗?我们可以在这个过程中修复它。

0投票

评论者:alexmill谢

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

0投票

评论者:alexmiller

这很复杂,不确定是否值得在1.10中查看。可能不会在1.10中查看它。

0投票

评论者:alexmill谢

感谢回答。在复杂性方面,我可以移除“优化”反射调用部分的代码,这样既简化了算法,又使反射点对编译器(从而对用户)可见。

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