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)}} 附近"),因此我谦卑地请求您的原谅,因为我没有阅读相关手册。
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

评论者:vladimirsitnikov

(link: ~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:这进一步证实了数据共享是应该首先避免的,而不是volatile{quote}

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

{quote}这张工单需要更明确的问题描述。即:“我在做____”(举例)”,这显示了Reflector.getMethods()作为瓶颈{quote}
这是真的。
我遇到的具体情况已被开发团队解决。
我只是认为一些基本的缓存可以让Clojure默认执行正确,并减少手动编写的类型专用化。

0

评论者:alexmiller

我注意到其中有很多“应该”类型的说法。关键是,没有这些变更是不会上线的,除非我们知道没有影响。但更重要的是,我甚至不会将其标记为已分类,除非它是一个好的工单,并且开始于一个问题陈述。

0

评论者:mikera

亚历克斯,“知道没有影响”是什么意思?表面上看,这可能是一个荒谬的立场,如果您的快速路径/常见案例的改进是显著的,那么允许在罕见情况下的轻微倒退,这是一种完全可接受的权衡。

此外,这绝对不是Clojure更改中普遍应用的基准。Clojure中有很多更改会导致其他方面的性能倒退,您只需看一下安迪·菲宁胡特(Andy Fingerhut)在GitHub上的出色基准测试工作,就能看到这一点: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
-我为反射性能创建一个单独的问题焦点工单
- 我将对不同情况下的变更进行基准测试
- 你将根据我们可以证明在常见情况下有明显的改进、所有测试都通过以及边缘情况(并发访问、频繁缓存未命中等)没有重大回归,对补丁进行分类

如果你想要一个问题导向的问题,我认为为每个“解决方案”创建单独的工单/补丁没有太多意义(尽管一些OSS项目选择这样做,但它们通常有更精简的过程来处理小的变化/优化,这可能不适合Clojure开发过程)

同意吗?

0

评论者:alexmiller

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

我将重申,如果问题可以通过类型提示解决,我认为进行任何这些工作都没有意义。

0
参考: https://clojure.atlassian.net/browse/CLJ-1784 (由alehreadport提交)
...