2024 年 Clojure 状况调查!中分享您的想法。

欢迎!请查阅 关于 页面以了解更多此平台的信息。

0
Clojure

目前 {{Reflector.getMethods}} 执行包含 {{java.lang.reflect.Method}} 复制的昂贵逻辑。
参见:https://github.com/clojure/clojure/blob/b8607d5870202034679cda50ec390426827ff692/src/jvm/clojure/lang/Reflector.java#L373

在我的应用程序中,我发现以下回溯

在 Reflector.copyMethods,在 Reflector.invokeInstanceMethod...

这类回溯是所有堆分配的第二大消费者。

JDK 不能缓存 {{Methods}} / {{Fields}},因为它们是 可变的(例如,用户可以在这里和那里调用 {{setAccessible}})。
然而,对于 Clojure 来说,我相信缓存 {{Methods}} 和 {{Fields}} 应该是完全可以接受的。

您怎么看?
例如,使用 WeakHashMap>>` 或更复杂的结构以处理 {{String name}}。

15 个答案

0

评论者:alexmiller

如果您发现 Reflector 是您应用程序的热点,您可能需要启用 * } 并使用类型提示来避免反射。

0
评论者:vladimirsitnikov

您的意思是clojure中永远不会使用反射吗?
我 understands that if developer gives enough type hints the reflection would go away.

但是
1) 我不知道是否容易实现(也就是说,如果它根本不可能,是否可能并且可行)
2) 我不确定“始终使用类型提示”是否被视为最佳实践。例如,[warn-on-reflection|https://docs.clojure.org/clojure.core/*warn-on-reflection*] 的文档页面并没有提到“始终使用类型提示”。
3) 缓存 {{copyMethods}} 似乎在这里很容易实现,这将为那些省略类型提示的人减少 CPU 循环。

PS. 我是一位 Java 性能工程师,而不是 Clojure 工程师(就像“我的 Clojure 知识接近 {{(+ x y)}}”一样),因此我诚恳地请求您原谅我没有做到 RTFM。
0

评论者:alexmiller

不,我是说,如果反射是您应用程序的热点,通常在热点区域添加类型提示花几分钟时间是值得的,这在 Clojure 应用程序中是常见的建议。一旦完成这项最小的工作,很少有 Clojure 应用的性能受到反射的限制。

缓存看起来是一个简单的解决方案,直到您考虑到所有管理方面。缓存如何清理?实例是否可变,能否重复使用?是否存在类加载器或代码重新加载会产生意外的副作用的情况?将共享资源放入调用路径中的并发效果是什么?缓存对内存的影响是什么?它可以配置吗?

这些都是需要调查的事情,这意味着这并不是低垂的果实。

0
by

评论者:mikera

简单缓存 Reflector.getMethods 调用的补丁

0

评论者:mikera

我创建了一个小补丁,为 Reflector.getMethods 调用添加非常简单的(固定大小,每个参数数量只有 1 个元素)缓存。目的是使这个缓存非常简单,以避免并发效应和可变大小缓存的问题。

这在我对在循环中调用相同方法的测试中帮助了一点点(大约 15-20%),这可能是人们真正关心反射性能的常见情况。

由于我认为大部分开销实际上是在 invokeMatchingMethod 中,因此性能肯定可以进一步提高。但这是一个独立的问题。这个补丁开辟了在该区域进一步性能优化的道路。

;; clojure 1.8.0-RC3
user=> (let (v (identity 1)) (time (dotimes (i 1000) (.doubleValue v))))
"Elapsed time: 1.598779 msecs"

;; 使用缓存的参数数量
user=> (let (v (identity 1)) (time (dotimes (i 1000) (.doubleValue v))))
"Elapsed time: 1.359888 msecs"

0
by

评论者:vladimirsitnikov

(链接:~mikera),我想知道您修补的结果是否会对并发工作负载的性能产生负面影响。

您已创建了一个争用点,因为许多线程将尝试更新 {{private static InstanceMethodCache[] instanceMethodCache}} 条目,因此它将遇到“真正共享”和“虚假共享”问题。

{{instanceMethodCache}} 是否应该是最后的并大写字母的?

0

评论者:alexmiller

本票需要一个更好的问题定义。即:“我做____”(附带示例),以显示 Reflector.getMethods() 是瓶颈。

如果我猜测问题,我仍然不相信这是最好的解决方案。

ThreadLocal 很可能是具有最低并发影响的最缓存解决方案。

0

评论者:mikera

这不应该有任何明显的并发影响:这种非常简单的方法不需要锁定。大多数时间它只是从堆上的数组中进行未锁定的读取,Java 内存模型足以保证正确的行为。这比threadlocal 更便宜,例如,这里有证据表明这是 10-20 倍更快:http://stackoverflow.com/questions/609826/performance-of-threadlocal-variable

至少,任何并发影响都非常小,将被经常避免 getMethods 调用的好处所淹没,这些调用是昂贵的。数组访问的成本是几纳秒,相比之下,getMethods 调用的成本据上表显示是几百纳秒。

我能想到的最糟糕的并发情况是两个不同的线程以高频率调用 不同 的方法,并且这些调用被完美地交织在一起,以至于它们始终使缓存失效。但即使在这种情况下,这可能也比当前的代码没有明显更差。

@Vladimir 是的,insntanceMethodCache 可以是最后的。我想这可以帮助 JVM 微微提高性能。

@Alex,我提出了这个补丁,因为它是目前所存在的改进,我肯定认为它不会是“最好的可能解决方案”。本着开源和逐步进步的精神,我希望您考虑接受它,即使这个问题保持开放以供将来考虑。这也与 clj-1866 有关,我试图以几种不同的方式改善反射的“快速路径”。如果您更愿意有包含大量改进的单个大补丁,我当然可以做到这一点,我有一种印象,那就是较小的、较“明显”的补丁更容易为您审查,但我两种都乐意接受。

0

评论者:vladimirsitnikov

(链接:~mikera),您没有理解要点。

请在此处检查:http://shipilev.net/blog/2014/jmm-pragmatics/#_benchmarks,第 77/100 幻灯片“SC-DRF: 写入”

{引用}Alexey Shipilev:这加强了这样一个观点,即您首先应该避免数据共享,而不是挥发物{引用}

拥有 {{ThreadLocal}} 缓存可以消除 "共享更新" 问题。

{quote}这个工单需要更好的问题定义。即:"我在执行____"(举例说明),显示 Reflector.getMethods() 作为瓶颈{quote}
这是真实的。
我的具体情况已由开发团队解决。
我只是想简单地认为一些基本的缓存会使得 Clojure 默认做好正确的事情,并减少手动编写的类型专用代码。

0

评论者:alexmiller

我看到了很多“应该”类型的声明。关键是,除非我们知道这样的变化没有影响,否则这类变化是不会被采纳的。但更重要的是,我不会在票证变成一个好的、以问题陈述开头的票证之前标记为已分类。

0

评论者:mikera

Alex,你所说的“知道没有影响”是什么意思?从表面上看,这似乎是一个荒谬的立场,如果你极大地改善了快速路径/常见情况,那么允许在罕见的角落情况中发生轻微的退步,这是一个完全可接受的权衡。

此外,这绝对不是一个在 Clojure 变更上普遍应用的准则。Clojure 中有很多其他改变会导致其他区域的性能退步,你只需要查看 Andy Fingerhut 优秀的基准测试工作就可以看到这一点: https://jafingerhut.github.io/clojure-benchmarks-results/Clojure-expression-benchmark-graphs.html

在我看来,任何与性能相关的内容的问题陈述都是显而易见的:“X的性能不佳,这对执行X的用户有影响。”如果你想要一个新工单/更改的描述,类似于这样,我很愿意这样做,但这只是官僚程序中的打勾。请把这视为对你贡献过程的的建设性反馈。

你具体需要看到哪些基准来作为这类性能相关问题改进的合理证明?

0

评论者:alexmiller

与 CLJ-1866 中的评论相似,这个工单的标题是“Reflector.getMethods 应该被缓存”。这同样是一个解决方案,而不是问题。我需要的是标题如“循环中的重复反射很慢”的标题,并以一些示例代码开始的问题描述。没有好的问题陈述,我无法对工单进行分类。我可能仍会考虑问题的优先级足够低,不足以值得一做——但我会先保留判断,直到工单得到改进。

先前变更产生意外性能影响的事实,使我进一步建议这项(以性能为导向)的问题单应验证其主张。您已添加代码,这让该代码的"错过"路径比之前更慢。慢多少?它应该使"命中"路径更快——快多少?在典型的代码中,我们多久会遇到"命中"与"错过"路径?我认为例子将展示一个"命中"路径是常见用例的情况。这些问题是作为筛选者,我必须问自己以评估任何建议的解决方案。

此外,您正在引入并发问题,需要更多的额外工作来验证正确性(当前补丁有可见性问题),以及您没有引入竞争或内存问题。这些问题是任何缓存优化相关的典型问题,我可以指出先前有许多补丁都在解决这些问题。

0

评论者:mikera

感谢Alex解释您的担忧。

我同意有针对性地解决问题的补丁更好,所以我建议以下
- 我们关闭此问题和clj-1866
- 我将为反射性能创建一个单独的问题聚焦的问题单
- 我将对多个不同情况的整体更改进行基准测试
- 您将根据假设我们对常见用例可以展示明显的改进、所有测试都像以前一样通过、而且在边缘情况(如并发访问、频繁缓存未命中等)中不出现重大退化来评估补丁

如果您希望有一个问题导向的问题,我不认为为每个"解决方案"创建单独的问题单/补丁有很大的意义(虽然一些开源项目选择这样做,但它们通常为微小的更改/优化有更流畅的流程,这可能不适合Clojure的开发流程)

同意吗?

0

评论者:alexmiller

正如我所说的,我们更喜欢小型聚焦的问题单和补丁,而不是一个大补丁。

我将重申,如果场景是类型提示就能解决这个问题的话,我不认为进行任何这项工作有意义。

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