Clojure 2024 状态调查! 中分享你的想法。

欢迎!请查看关于页面了解此功能的工作方式。

0
Clojure

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

在我们的应用程序中,我看到以下回溯:

在 Reflector.copyMethods at Reflector.invokeInstanceMethod at ...

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

JDK 不能缓存 {{Methods}} / {{Fields}},因为它们是 可变的(例如,用户可以在此处和那里调用 {{setAccessible}})。
然而,为了 Clojure 的目的,我认为缓存 {{Methods}} 和 {{Fields}} 是可以接受的。

你有什么看法?
例如 WeakHashMap<Class, WeakReference<List<Method>>> 或者更复杂的数据结构来考虑 {{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)}} 的水平”),所以我恳请各位在我没有阅读相关文档的情况下多多包涵
0

评论人:alexmiller

不,我想说的是,如果反射是您应用程序的一个热点,通常在那些热点区域添加类型注解值得花几分钟时间,这对于Clojure应用程序来说是一条常见的建议。一旦完成了这项最小的工作,很少有Clojure应用程序会受到反射的限制。

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

所有这些都是需要调查的事情,这意味着这并不是轻而易举的事情。

0

评论者:mikera

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

0

评论者:mikera

我为在Reflector.getMethods调用上添加一个非常简单的(固定大小,每个arity一个元素)缓存创建了一个小的补丁。目标是使其非常简单,以避免并发效应和可变大小缓存等问题。

在测试中,这帮助我在反射调用循环中的方法时节省了少量时间(大约15-20%),这很可能是人们真正关心反射性能的常见情况。

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

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

;; 带有缓存的arity
user=> (let (link: v (identity 1)) (time (dotimes (link: i 1000) (.doubleValue v))))
"执行时间:1.359888 毫秒"

0

评论由:vladimirsitnikov 发布

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

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

是否应该将 {{instanceMethodCache}} 声明为 final 并使用大写字母?

0

评论人:alexmiller

此工单需要更好的问题定义。即:“我在做 ____”(附上示例)以展示 Reflector.getMethods() 成为瓶颈。

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

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

0

评论者:mikera

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

至少,任何并发影响都非常小,将被经常避免 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}Alexey Shipilev:这加强了这样一个观点,即你应该首先避免数据共享,而不是易变性的问题{quote}

拥有{{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 报告)
...