请在 2024 年 Clojure 状态调查中分享您的想法!

欢迎!请访问 关于 页面了解更多此问答功能的详细信息。

0
Clojure

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

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

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

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

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)}} 附近”),所以我恳请你们原谅我没有按时查阅文档。
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}} 应该是最终的并且以大写字母编写吗?

0

由 alexmiller 发布的评论

这张工单需要一个更好的问题定义。即:"我正在做 ____"(附带一个示例),显示 Reflector.getMethods() 是瓶颈。

如果我猜想问题是什么,我仍然不认为这是最好的解决方案。

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

0

评论者:mikera

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

至少,任何并发影响都非常小,将远远小于经常避免昂贵的 getMethods 调用的好处。与 getMethods 相比,数组访问的成本仅为几纳秒。

我能想到的最糟糕的并发情况是:两个不同的线程以高频率调用 getMethods 并对这些调用进行完美的交错,以至于它们始终使缓存失效。但即使在这种情况下,它可能也不比当前的代码差。

@Vladimir 是的,instanceMethodCache 可能是最终的。我猜这可能有助于 JVM 比微小的改进。

@Alex,我提出这个补丁是因为它与目前的内容有所改进,我确实不认为它将是“最好的解决方案”。在开源和逐步改进的精神下,我希望您考虑接受它,即使这个问题仍然开放以供未来考虑。这也与 clj-1866 链接在一起,我在尝试以几种不同的方式改进反射的“快速路径”。如果您更愿意有一个包含大量改进的单一大补丁,我当然可以做到,我印象中较小的、更“明显”的补丁更容易审阅,但都行。

0

评论由:vladimirsitnikov

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

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

{引用}Alexey Shipilev:这加强了我们应首先避免数据共享的观点,而不是volatile{引用}

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

{引用}此工单需要更好的问题定义。主要是:“我在执行____”(提供示例),显示了 Reflector.getMethods() 为瓶颈{引用}
这是真的。
我的特定情况已经被开发团队解决了。
我只是认为一些基本的缓存可以让Clojure默认执行正确,并且减少手动编写的类型特殊化。

0

由 alexmiller 发布的评论

我注意到其中有大量应该类型陈述。关键在于,我们知道此类变化没有影响之前,不会进行任何此类更改。更重要的是,我甚至不会将其标记为已分派,除非它是一个具有问题声明的优秀工单。

0

评论者:mikera

亚历克斯,你说的“知道没有影响”是什么意思?表面上看这似乎是一种荒谬的观点,在明显的情况下,在普遍应用的Clojure更改标准中,如果你能显著提高快路径/常用案例,允许罕见的角落案例出现影响是很可接受的。

此外,这绝对不是对Clojure更改普遍适用的标准。Clojure中有许多更改会导致其他区域性能下降,你只需要看看安迪·菲尼胡特的出色基准测试工作就能看到这一点: https://jafingerhut.github.io/clojure-benchmarks-results/Clojure-expression-benchmark-graphs.html

在我看来,性能相关的问题显然需要一个问题声明:“X的性能不佳,这会伤害正在做X的用户。”如果你想要一个具有类似说法的新工单或更改描述,我会很高兴做,但这只是感觉像官僚主义的框子勾选。请把这视为对你贡献过程的建设性反馈。

你究竟需要看到哪些基准作为性能相关问题如此类改进的有效证明?(i.e. 哪些基准)?

0

由 alexmiller 发布的评论

类似于我在CLJ-1866中的评论,这个问题的标题是"Reflector.getMethods应该被缓存"。这又是一个解决方案,而不是一个问题。我正在寻找一个像"循环中重复反射很慢"这样的标题,并需要一个以示例代码开头的描述,展示这个问题。如果没有一个好的问题陈述,我无法对这个工单进行归档。我可能会考虑问题的优先级足够低,以至于现在不值得归档——但我会保留判断,直到工单得到改进。

先前变更产生的意外性能影响事实仅仅增加了我对(性能导向)这个工单应验证其主张的建议的信任度。你已经增加了代码,使得这个代码的"未命中"路径比之前要慢。慢多少?它应该加快"命中"路径——加快多少?在典型代码中,我们多久会遇到"命中"与"未命中"路径?我的假设是,示例将展示一个命中路径很常见的场景。这些是我作为筛选者必须提出的问题,以评估任何拟议的解决方案。

此外,你引入了并发问题,并且需要更多的工作来验证正确性(当前补丁存在可见性问题)以及你没有引入竞争或内存问题。这些问题是任何与缓存相关的优化的典型问题,我可以指出任何数量之前与之作斗争的工单。

0

评论者:mikera

感谢Alex解释你的担忧。

我同意以面向问题的补丁方法更好,所以我建议以下做法
- 我们关闭这两个问题,clj-1866
- 我将创建一个针对反射性能的单独问题工单
- 我将对不同案例进行整体变更的基准测试
- 你将对补丁进行归档,假设我们可以证明常见情况下的显著改善,就像以前一样,所有测试都通过,并在边缘情况(并发访问,频繁的缓存未命中等)中没有出现重要的退步

如果你想一个问题导向的问题,我认为为每个"解决方案"创建单独的工单/补丁没有太多意义(尽管某些开源项目选择这种方式,它们通常有一个更加简化的处理小更改/优化的流程,这可能不适合Clojure开发流程)

同意吗?

0

由 alexmiller 发布的评论

正如我所说,我们更倾向于小而专注的工单和补丁,而不是一个大补丁。

我再次强调,如果场景是一个类型提示就能解决问题,那么我认为进行任何这些工作都没有意义。

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