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 中使用反射吗?
我明白,如果开发者提供了足够的类型提示,反射就会消失。

然而
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

评论由:mikera 提出

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

0

评论由:mikera 提出

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

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

由于我认为大部分开销实际上是在 invokeMatchingMethod 中,因此性能肯定可以进一步改进。这一补丁开辟了在该领域中进一步性能优化的道路。

;; clojure 1.8.0-RC3
user=> (let (link: v (identity 1)) (time (dotimes (link: i 1000) (.doubleValue v))))
"执行时间:1.598779 毫秒"

;; 使用缓存后的参数
user=> (let (link: v (identity 1)) (time (dotimes (link: i 1000) (.doubleValue v))))
"执行时间:1.359888 毫秒"

0
by

评论者:vladimirsitnikov

(链接:~mikera),我想知道您的补丁是否会导致并发工作负载的性能下降。

您创建了一个冲突点,因为许多线程都会尝试更新 {{private static InstanceMethodCache[] instanceMethodCache}} 条目,这将同时引发“真实共享”和“虚假共享”问题。

{{instanceMethodCache}} 是否应该是 final 的,并且应该使用大写字母?

0
by

评论由:alexmiller 撰写

此工单需要一个更好的问题定义。也就是说:“我正在做____”(用例子),展示了 Reflector.getMethods() 作为瓶颈。

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

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

0
by

评论由:mikera 提出

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

至少,任何并发影响都非常小,以至于会被经常避免 expensive 的 getMethods 调用的好处所淹没。数组的访问成本只是几个纳秒,而 getMethods 的成本根据上面的基准测试来看是几百纳秒。

我能想到的最坏并发情况是在两个不同的线程以高频率调用不同方法的 getMethods 的情况下,这些调用完美交织,因此总是会使缓存无效。但即使在那种情况下,这也可能不会比当前的代码更糟糕。

@Vladimir 是的,instanceMethodCache 可以被 final 化。我想这可能对 JVM 有很小的帮助。

@Alex,我提出这个补丁是因为它与目前的情况相比有所改进,我确实不认为它将是“最好的解决方案”。本着开源和逐步进步的精神,我希望您考虑接受它,即使这个问题仍然开放以供将来考虑。这也与 clj-1866 有关,我试图从几个不同的方面更好地改进“快速通道”的反射。如果您更愿意有一个包含许多改进的单个大补丁,我当然可以做,但我有印象,更小、更“明显”的补丁更容易让您审查,但愿意任由您决定。

0

评论者:vladimirsitnikov

(链接:~mikera),你没有抓住重点。

请在此处检查:http://shipilev.net/blog/2014/jmm-pragmatics/#_benchmarks,第77/100页 "SC-DRF: 写入"

{quote}亚历克谢·希皮列夫:这加强了这样一个观点:首先应该避免的是数据共享,而不是易变性{quote}

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

{quote}这个工单需要一个更好的问题定义。那就是:"我在做____"(附带示例),显示 Reflector.getMethods() 是瓶颈{quote}
这是真的。
我的特殊情况已通过开发团队以某种方式解决。
我只是认为一些基本的缓存可以让 Clojure 默认做正确的事情,并减少手动编写的手动类型专业化的数量。

0

评论由:alexmiller 撰写

我在那里看到了很多 "应该" 类型的声明。关键是,在没有知道这些改变不会有影响之前,我们将不会对此类更改进行任何更改。但更重要的是,只有在它是一个良好的工单,并以问题声明开始时,我才会将其标记为已评审。

0

评论由:mikera 提出

亚历克斯,你说的 "知道没有影响" 是什么意思?表面上看,这似乎是一个荒谬的立场,如果你在显著提高快速路径/常见情况的同时允许罕见角落情况发生一些轻微的倒退,这是一个完全可接受的权衡。

此外,这绝对不是Clojure更改所普遍应用的标准。Clojure 中有很多更改导致其他区域的性能倒退,你只需要看看安迪·芬格胡特出色的基准测试工作: 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 报告)
...