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次调用中。
             平均执行时间: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.999273 ns
    执行时间标准差:0.112703 ns
   执行时间下四分位数:6.817782 ns ( 2.5%)
   执行时间上四分位数:7.113845 ns (97.5%)
                   使用的开销:2.477107 ns


*原因:内联版本仅适用于参数数量为2的情况,其他情况则进行反射。

16 答案

+2

评论者:ypeleg

Bump。我刚刚非常不幸地遇到了这个问题。

这里是两个独立的问题
1) (aget 2d-array-doubles 0 0 ) 不会发出反射警告。
2) 它看起来编译器已经有了足够的信息来避免在这里进行反射调用。

-注意,随着维数的增加,这会变得更糟,例如(获取 doubles3d 0 0 0)-
-将会慢1M,等等'- 不正确,除非你迭代过所有元素。实际上是
仅仅是对每个查找进行n_dims的1000次幂。

这是一个令人震惊的惊喜,尤其是考虑到你经常为了速度而使用原始数组,
并且一个常见的用例是内部循环(s)迭代数组。

0 表票
_评论者:gfredericks_

查看源代码可以清楚地看出,这个假设是正确的 -- 内联版本只适用于2个参数,否则将进行反射。

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

评论者:gfredericks

我可能对此有所尝试。

0 表票

评论者:gfredericks

我认为在不更改编译器其他地方的代码的情况下,几乎不可能解决这个问题,因为{{aget}}中进行的反射与正常Clojure反射不同 -- 它显式地在函数体中,而不是由编译器生成。因为编译器没有生成它,所以它根本不知道它的存在。所以即使{{aget}}对其他参数类型进行了修复,你仍然不会在没有内联时得到警告。

我可以想象在函数上添加某种元数据,以便告诉编译器如果未内联则将进行反射。或者可能是一个更通用的未内联警告?

添加另一个编译器选项的全局范围似乎与数组和函数无法对反射发出警告的严重性平衡。

0 表票

评论者:gfredericks

附带CLJ-1289-p1.patch,该文件简单地将对aget的变长调用内联。它假设如果看到数组参数上的{{:tag}}是一个以{{[}}开头的字符串,则可以假设对{{aget}}的一次调用的返回值可以用同一个字符串标记,带头{{[}}的部分将被去除。

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

0 表票
by

评论者:alexmiller

我认为这可能是真的,但一个更正式的方法是获取数组类并请求Class.getComponentType()(比字符串魔术更干净利落)。

0 表票
by

评论者:gfredericks

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

0 表票
by

评论者:gfredericks

I see (-> s (Class/forName) (.getComponentType) (.getName)) does the same thing -- is that route preferred, or is there another one?

0 表票
by

评论者: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 表票
by

评论者:alexyakushev

重新上传补丁:进行了一些小修复。

0 表票
by

评论者:alexyakushev

顺便问一下,为什么{{aset}}的文档字符串说它只适用于引用类型数组?这个注释过时了吗?我们可以沿途修复它。

0 表票
by

评论者:alexyakushev

有希望在1.10版本中审查这个吗?或许实现过于复杂,需要更多时间考虑?我可能尝试降低范围。

0 表票
by

评论者:alexmiller

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

0 表票
by

评论者:alexyakushev

谢谢你的回复。关于复杂度,我可以移除“优化”反射调用的一部分,这简化了算法并使编译器(从而是用户)能够看到反射位置。

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