请在 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 调用添加非常简单的(固定大小,每个参数个数一个元素)缓存。目标是保持其非常简单,以避免并发效应和可变大小缓存等问题。

在我的测试中,这有助于提高一点性能(大约 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:这强调了最初应该避免数据共享,而不是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

对于任何与性能相关的问题,我认为问题声明显然是明显的:"X的性能不佳,这伤害了正在做X的用户。" 如果你需要一个像这样的话的新工单/更改描述,我将非常乐意这样做,但这完全感觉像是官僚主义的形式主义。请将此视为对你贡献流程的 Constructive feedback。

你具体(即哪些基准)需要看到作为这个性能相关问题上改进的合理证明?

0

评论者:alexmiller

与我在CLJ-1866中提出的评论相似,这张工单的标题是"Reflector.getMethods应该被缓存"。这又是一个解决方案,而不是问题。我寻找的是像"循环中重复反射很慢"这样的标题,以及一个以示例代码开头的描述来展示问题。没有良好的问题陈述,我就无法对这张工单进行分类。我可能仍然认为这个问题的重要性足够低,以至于现在不值得分类——但我将保留最终判断,直到工单得到改进。

之前更改对性能产生意外影响的事实,进一步佐证了我的建议,即这个(以性能为导向)的工单应该验证其主张。你已经添加了代码,这使得此代码的"未命中"路径比之前慢。慢了多少?它应该使"命中"路径更快——快多少?在典型的代码中,我们多久遇到一次命中与未命中的路径?我的假设是示例将展示一个命中路径很常见的场景。作为筛选者,这些是我必须询问的问题,以评估任何拟议的解决方案。

此外,你正在引入并发问题,并且需要进行一些额外的工作来验证正确性(当前的补丁存在可见性问题)以及你未引入竞争或内存问题。这些是任何与缓存相关的优化典型的疑难问题,我可以指向任何数量的先前工单,它们都与此作斗争。

0

评论者:mikera

感谢Alex解释你的担忧。

我同意,问题导向的补丁方法更好,所以我建议以下内容
- 关闭这两个问题和clj-1866
- 为反射性能创建一个单独的问题集中工单
- 我会对大量不同情况进行整体更改的基准测试
- 你将基于我们可以在常见情况下证明有显著改进的假设进行补丁分类,所有测试都像以前那样通过,且在边缘情况(并发访问、频繁缓存未命中等)中没有发生主要退化

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

同意吗?

0

评论者:alexmiller

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

我将重申,在类型提示可以解决问题的情况下,进行任何此类工作都不一定有意义。

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