请在 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: Final GC required 3.5198021166354323 % of runtime
WARNING: Final GC required 29.172288684474303 % of runtime
评估次数 : 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))
WARNING: Final GC required 40.59820310542545 % of runtime
Evaluation count : 62135436 in 6 samples of 10355906 calls.
             Execution time mean : 6.999273 ns
    Execution time std-deviation : 0.112703 ns
   Execution time lower quantile : 6.817782 ns ( 2.5%)
   Execution time upper quantile : 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中。它假设如果在数组参数上看到一个以{{[}}开始的字符串{{:tag}},则可以假设从一次调用到{{aget}}的返回值可以用相同的字符串标记,并且leading {{[}}将被去除。

我并非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)。最好将其转换为测试,但我还不知道如何操作。

顺便说一下,我不得不使用一个hack {{((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报告)
...