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 中 at 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)}}附近”),所以我恳请各位原谅我没有查阅官方文档。
0

由 alexmiller 发布的评论:

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

缓存似乎是一个简单的解决方案,直到您考虑所有的管理方面。缓存如何清理?实例是否可变且可重用?是否存在类加载器或代码加载可能会产生意外副作用的情况?将共享资源放在调用路径中的并发效应如何?缓存的内存影响是什么,它是否可配置?

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

0

评论人:mikera

对于Reflector.getMethods调用的小arity缓存补丁

0

评论人:mikera

我创建了一个小的补丁来添加非常简单的(固定大小,每个arity一个元素)缓存,用于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毫秒”

;; 使用缓存的arity
user=> (let (link: v (identity 1)) (time (dotimes (link: i 1000) (.doubleValue v))))
“经过时间:1.359888毫秒”

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相比,数组访问的成本只是几纳秒。

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

@Vladimir是的,instanceMethodCache可以被设为final。我想这可能会对JVM非常轻微地有所帮助。

@Alex,我提出这个补丁是因为它与目前存在的方法相比是一个改进,我当然不认为它将是“最佳解决方案”。在开源和逐步进步的精神下,我希望您考虑接受它,即使这个问题仍然为未来考虑而保持开放。这也与clj-1866有关,我正在尝试以几种不同方式改善“快速路径”的反射。如果您想要一个具有大量改进的单一个大补丁,我当然可以这样做,但据我所知,较小的、更“明显”的补丁更容易供您审查,但我两种方式都愿意。

0

评论者:vladimirsitnikov

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

请查看这里: http://shipilev.net/blog/2014/jmm-pragmatics/#_benchmarks,第77/100页 "SC-DRF: 写入"

{quote}亚历克谢·希皮列夫:这强化了这样的想法,你应该首先避免数据共享,而不是volatile{quote}

拥有 {{ThreadLocal}} 缓存将消除 "共享更新" 问题。

{quote}这个工单需要一个更好的问题定义。即:"我在做 ____"(附带示例),展示了Reflector.getMethods() 是瓶颈{quote}
这是真的。
我的特殊情况已被开发团队解决。
我只是认为一些基本的缓存会让 Clojure 默认做正确的事情,并且需要更少的类型专用化手动编写。

0

由 alexmiller 发布的评论:

我在这里看到了很多 "应该" 类型的陈述。关键是,除非我们知道没有影响,否则这种改变是不会被采纳的。但更重要的是,我甚至不会将其标记为已分类,除非它是一个好的工单,并且从一个问题陈述开始。

0

评论人:mikera

亚历克斯,你说的 "知道没有影响" 是什么意思?表面上看,这似乎是一个荒谬的立场,如果显著提高了快速路径/通用案例,允许在罕见的角落案例中出现轻微的退化,这是一个完全可以接受的权衡。

此外,这绝对不是 Clojure 变更中普遍应用的标准。很多变更都会进入 Clojure,导致其他领域的性能下降,你只需要看看安迪·芬格胡特的出色基准测试工作,即可看到这一点: https://jafingerhut.github.io/clojure-benchmarks-results/Clojure-expression-benchmark-graphs.html

在我看来,任何与性能相关的问题陈述都是显而易见的:"X 的性能不佳,这伤害了正在执行 X 的用户。" 如果你想要一个新工单/更改描述,表明类似的内容,我将非常乐意这样做,但这感觉更像是对贡献流程的官僚主义行为。请将此视为对您的贡献过程的 constructve 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报告)
...