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)}}”附近),所以我衷心请求您原谅我未能及时查阅手册。
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"

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

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是,insntanceMethodCache可以是final的。我猜这可能会非常轻微地帮助JVM。

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

0 投票

评论者:vladimirsitnikov

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

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

{quote}Alexey Shipilev:这强化了这样一个观点,首先应该避免数据共享,而不是 volatiles{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

感谢Alex解释您的担忧。

我同意,对补丁采取面向问题的方法是更好的,所以我建议以下这些:
- 我们关闭这个问题和clj-1866
- 我将为反射性能创建一个独立的问题焦点票据
- 我将对不同的多个情况整体进行基准测试
- 您将根据我们的假设对补丁进行分类,即我们可以在常见的案例中证明有明显的改进,所有测试通过,并且在边缘案例中(并发访问、频繁的缓存未命中等)没有发生错误退化

如果您想得到一个问题导向性的问题,那么我认为为每个“解决方案”创建单独的票据/补丁并没有太多意义(尽管一些开源项目选择这样做,他们通常为次要更改/优化有一个更简洁的过程,这可能不适合Clojure开发过程)

同意吗?

0 投票

评论者:alexmiller

正如下所述,我们更喜欢小型专注的票据和补丁,而不是一个大补丁。

我将再次重申,如果场景可以通过类型提示解决问题,那么进行这些工作我并不确信是有意义的。

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