请分享您的想法,参与 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)}}”的水平),所以我恳请各位原谅我未能完成RTFM(阅读官方文档)。
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))))
"Elapsed time: 1.598779 msecs"

;; 使用缓存的arity
user=> (let (link: v (identity 1)) (time (dotimes (link: i 1000) (.doubleValue v))))
"Elapsed time: 1.359888 msecs"

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: Writes"(写入)

{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

感谢亚历克斯解释你的担忧。

我同意以问题为导向的方法来处理补丁,因此我建议以下内容:
- 我们关闭此问题和clj-1866
- 我将创建一个针对反射性能的独立问题相关的票
- 我将为无数不同案例对更改进行基准测试
- 你将对补丁进行分类,假设我们可以证明在常见情况下有明显的改进,所有测试都像之前一样通过,并且在边缘情况下(并发访问、频繁缓存未命中等)没有出现重大回归

如果你想提出一个以问题为中心的问题,那么我认为为每个“解决方案”创建单独的票据/补丁没有太多意义(尽管一些开源项目选择这样做,但它们通常有一个更简洁的过程来处理小更改/优化,这可能不适合Clojure开发过程)

同意吗?

0
通过

评论者:alexmiller

正如我说的,我们更倾向于小型集中的票和补丁,而不是一个大补丁。

我将重申,如果我们假设类型提示可以解决这个问题,那么进行这些工作似乎没有太大的意义。

0
参考: https://clojure.atlassian.net/browse/CLJ-1784 (报告人:alex+import)
欢迎来到Clojure问答区,您可以在这里提问并获得Clojure社区成员的回答。
...