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<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)}}”)。因此,我恳请您原谅我没有按照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

评论由: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 的成本从上面的基准测试来看是几百纳秒。

我能想到的最差的并发情况是两个不同的线程在高速率调用 getMethods,而这些调用是完美交错进行的,以至于它们总是会使缓存无效。即使是这种情况,它可能也未必比当前的代码更糟糕。

@Vladimir 是的,insntanceMethodCache 可以是最终的。我想这可能会对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 优秀的基准测试 efforts 来了解情况: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报告)
...