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))
警告:最后的 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。


user=> (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的1000次幂。

令人惊讶的是,特别是考虑到你通常会为了速度而访问原始数组,
且常见的用例是内部循环遍历数组。

0投票
_评论者:gfredericks_

看一眼源代码,就可以明显看出假设是正确的--内联版本仅适用于2元函数,否则它反映的是。

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

评论者:gfredericks

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

0投票

评论者:gfredericks

我认为要解决反射警告问题,在不更改编译器的其他地方代码的情况下几乎是不可能的,因为在{{aget}}中执行的反射与正常的clojure反射不同--它是显式地在函数体中,而不是由编译器产生的。由于编译器没有产生它,所以它无法合理地知道它的存在。因此,即使{{aget}}对其他阶数进行了修复,您在不在内联时也不会收到警告。

我可以设想在函数上放置某种类型,告诉编译器它将反射如果不在内联中。或许更通用的提示未内联警告?

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

0投票

评论者:gfredericks

我附上了CLJ-1289-p1.patch,该文件简单地将.get的变元调用内联。它假设如果在数组arg上看到 {{:tag}},是一个以 {{[}} 开头的字符串,它就可以假设来自 {{aget}} 的一个调用返回的值可以被相同字符串标记,并移除了前导 {{[}}。

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

0投票

评论由: alexmiller 提出

我认为这可能是真的,但更官方的提问方式是获取数组类,并调用 Class.getComponentType()(比字符串魔术更简洁)。

0投票

评论者:gfredericks

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

0投票

评论者:gfredericks

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

0投票

评论由: alexyakushev 提出

我尝试按照(链接:~alexmiller)的建议实现。附带的补丁文件使用 getComponentType 代替字符串魔术,与第一个补丁相比,提出了以下改进:

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

不利的一面是,现在对 aset 的多参数调用将触发编译器反射,以前它是 {{Array.set}}。我想知道是否应该对这些角落进行平滑处理,或者在这种情况下,是否更好的是让这种情况下的反射更加明显(显示在编译器警告中)。

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

顺便提一下,我必须使用技巧 {{((var aget) ...)}} 来强制调用非内联版本。我并不喜欢它,但我还没有想出更好的办法。另一种方法是将非内联版本分离到单独的私有函数中,但我不想在核心命名空间中添加更多的 Vars。

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 报告)
...