请在 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) 我不知道这是否容易做到(换句话说,如果 überhaupt 可能,可维护性等)
2) 我不确定“始终使用类型提示”是否被认为是一种最佳实践。例如,[警告反射|https://docs.clojure.org/clojure.core/*warn-on-reflection*] 文档页面并没有提到“始终使用类型提示”
3) 缓存 {{copyMethods}} 很像是一个低垂的果实,这样那些省略了类型提示的人就能节省CPU周期

PS. 我是一名Java性能工程师,而不是Clojure工程师(我这方面的Clojure知识大概相当于 {{(+ x y)}}),所以如果我没有参照文档(RTFM),恳请您原谅。
0 投票
by

由 alexmiller 发布的评论:

不,我要说的是,如果您应用程序中反射是一个热点,通常在热点区域添加类型提示花几分钟时间是值得的,这通常是Clojure应用程序的常见建议。一旦完成这项基础工作,很少有Clojure应用程序会受到反射的限制。

缓存似乎是一个简单的解决方案,但在您考虑所有管理方面后,会怎样呢?缓存是如何被清理的?实例是否是可变的,并且可以重用?是否存在类加载器或代码重新加载产生的意外副作用?将共享资源放在调用路径上有哪些并发影响?缓存对内存的影响是什么?是否可配置?

这些都是需要调查的事情,这意味着这并非低垂的果实。

0 投票
by

评论者:mikera

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

0 投票
by

评论者: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))))
"Elapsed time: 1.598779 msecs"

;; with cached arities
user=> (let (link: v (identity 1)) (time (dotimes (link: i 1000) (.doubleValue v))))
"Elapsed time: 1.359888 msecs"

0 投票

评论者:vladimirsitnikov

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

您创建了一个热点问题,因为许多线程会尝试更新 {{私有静态实例方法缓存 InstanceMethodCache[] instanceMethodCache}}条目,因此它将遇到“真正的共享”和“虚假的共享”问题。

{{instanceMethodCache}}应该是final的并且应该用大写字母书写吗?

0 投票

由 alexmiller 发布的评论:

此票证需要一个更好的问题定义。即:“我正在做____”(带有一个示例),这显示了Reflector.getMethods()作为瓶颈。

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

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

0 投票

评论者:mikera

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

至少,任何并发影响都非常小,将被频繁避免getMethods调用(这是一种昂贵的调用)的好处所淹没。与getMethods相比,数组访问的成本只是几纳秒,而从上面的基准测试来看,getMethods的成本是几百纳秒。

我能想到的最糟糕的并发情况是两个不同的线程以高频率在不同的方法上调用getMethods,这些调用完美地交织在一起,总是使缓存失效。但即使在这种情况下,它可能也比当前的代码不可测量地更糟。

@Vladimir 是的,insntanceMethodCache可以是final的。我想这可能会略微帮助JVM。

@Alex,我提出这个补丁是因为它与目前的情况相比是一个改进,我当然不认为它将是“最好的可能解决方案”。本着开源和逐步改进的精神,我希望您考虑接受它,即使这个问题留给未来的考虑。这还与clj-1866相关,我正在尝试以几种不同的方式改进反射的“快速路径”。如果您更愿意有一个包含大量改进的单个大补丁,我当然可以这样做。我印象中,更小、更“明显”的补丁更容易让您审查,但两种方式都可以。

0 投票

评论者:vladimirsitnikov

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

请在此处检查:[http://shipilev.net/blog/2014/jmm-pragmatics/#_benchmarks](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

Alex,你所说的“知道没有影响”是什么意思?从表面上来看,这似乎是一个非常不合逻辑的立场,如果你显著提高了快速路径/常见情况,那么允许在罕见边缘案例中存在轻微的回归是一个完全可以接受的权衡。

此外,这绝对不是Clojure变更的通用标准。有大量的变更引入Clojure,却导致其他区域的性能回归,你只需要看看 Andy Fingerhut 在这里的出色基准测试努力就可以看到这一点:[https://jafingerhut.github.io/clojure-benchmarks-results/Clojure-expression-benchmark-graphs.html](https://jafingerhut.github.io/clojure-benchmarks-results/Clojure-expression-benchmark-graphs.html)

在我看来,任何与性能相关的问题的陈述都是显然的:"X的性能不佳,这伤害了正在做X的用户。"如果你需要新的工单/更改描述,使其类似于这样,我将很高兴这样做,但这感觉简直就是官僚主义的形式主义。请将此视为对您的贡献过程的反馈。

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

0 投票

由 alexmiller 发布的评论:

正如我在CLJ-1866中的评论一样,此工单的标题是“Reflector.getMethods 应该被缓存”。这又是一个解决方案,而不是问题。我正在寻找一个类似“循环中重复反射速度慢”的标题,并希望描述从一些代码示例开始,以展示存在的问题。如果没有好的问题陈述,我无法进行工单分级。我可能仍会考虑该问题的优先级足够低,以至于我们现在不必分级 - 但我会等到工单改进后再做出判断。

之前变革产生的意外性能影响的事实只是增加了我对这个(性能导向)工单应验证其诉求的建议。你已经增加了代码,这使得代码的“未命中”路径比之前慢。慢多少?它应该使“命中”路径更快 - 快多少?在典型代码中,我们多久会遇到一次“命中”和“未命中”路径?我的推测是,示例将演示一个命中路径常见的案例。作为筛选者,我必须提出这些问题来评估任何拟议的解决方案。

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

0 投票

评论者:mikera

感谢Alex解释你的担忧。

我同意问题导向的补丁方法更好,所以我建议以下做法
- 我们关闭此问题和 clj-1866
- 我将创建一个关于反射性能的问题聚焦的单独工单
- 我将为多种不同案例对更改进行基准测试
- 你会假设如果我们在常见情况下能够证明可感知的改进,并且所有测试都与之前一样通过,以及没有在角落案例(并发访问、频繁缓存未命中等)中出现主要回归,那么可以对补丁进行分级

如果你需要一个以问题为导向的工单,那么我不想在针对每个“解决方案”创建单独的工单/补丁上做任何事情(尽管一些开源项目会选择这样做,但它们通常有一个更简化的流程用于对较小的更改/优化,这可能不适合Clojure 开发流程)

同意吗?

0 投票

由 alexmiller 发布的评论:

正如我所说的,我们更愿意做小而专注的工单/补丁,而不是一个大补丁。

我将重申,如果场景中一个类型提示就能解决问题,那么我不认为做任何这项工作都有意义。

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