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
Evaluation count : 63558 in 6 samples of 10593 calls.
             Execution time mean : 9.457308 µs
    Execution time std-deviation : 126.220954 ns
   Execution time lower quantile : 9.344450 µs ( 2.5%)
   Execution time upper quantile : 9.629202 µs (97.5%)
                   Overhead used : 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%)
                   Overhead used : 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_

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

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

评论者:gfredericks

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

0

评论者:gfredericks

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

我想象可以有一种某些元数据,你可以把它加到一个函数上,告诉编译器它如果没有内联将会反射。或者也许一个更通用的未内联警告?

考虑全局作用域,增加编译器标志的严重性似乎与数组函数无法在反射时发出警告的严重性平衡。

0

评论者:gfredericks

附加的CLJ-1289-p1.patch将aget的可变调用简单内联。它假设如果它看到数组arg上的{{: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}}。
  • 如果内联版本在任何时候都无法解析数组的类型,则它会生成一个对非内联{{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](https://clojure.atlassian.net/browse/CLJ-1289) (由 alex+import 报告)
...