2024 年 Clojure 状态调查 中分享您的想法!

欢迎!请参阅关于页面以获得更多关于该工具的信息。

0 投票
Clojure

目前 {{Reflector.getMethods}} 执行的逻辑很复杂,包括处理 {{java.lang.reflect.Method}} 的复制。
见: https://github.com/clojure/clojure/blob/b8607d5870202034679cda50ec390426827ff692/src/jvm/clojure/lang/Reflector.java#L373

在我的应用程序中,我看到了以下回溯

at Reflector.copyMethods at Reflector.invokeInstanceMethod at ...

这类回溯是所有堆分配的第二大消费者。

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周期。

附注。我是一个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

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

你已经创建了一个热点,因为许多线程将尝试更新 {{private static InstanceMethodCache[] instanceMethodCache}} 条目,因此它将同时遇到“真正的共享”和“虚假的共享”问题。

{{instanceMethodCache}} 是否应声明为final,并使用大写字母命名?

0 投票

评论由:alexmiller 完成

此问题需要更好的问题描述。也就是说:“我在做 ____”(附上一个示例),以显示 Reflector.getMethods() 作为瓶颈。

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

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

0 投票

评论者:mikera

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

至少,任何并发影响都非常小,将远小于避免昂贵的getMethods调用的好处。与上面基准测试中出现的getMethods相比,数组访问的成本只是几个纳秒。

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

@Vladimir 是的,instanceMethodCache 可以是final。我猜这可能会让JVM微乎其微地有所帮助。

@Alex,我提出这个补丁是因为它优于目前的版本,我当然不认为它将是“最佳解决方案”。本着开源和逐步进步的精神,我希望你考虑接受它,即使这个issue仍然可以留待未来考虑。这也与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的用户。”如果您希望有新的工单/更改描述说类似于这样的内容,我非常乐意这样做,但这真的感觉像是官僚主义的程序。请将此视为对您贡献过程的 constructingive 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 报告)
...