Clojure 2024 调查 ![图标]! 中分享您的想法。

欢迎!有关如何使用本站更多信息,请参阅 关于 页面。

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

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))))
"消耗时间:1.598779 msecs"

;; 带有缓存arity
user=> (let (link: v (identity 1)) (time (dotimes (link: i 1000) (.doubleValue v))))
"消耗时间: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 的成本根据上述基准测试来看,为几百纳秒。

我能想到的最坏并发情况是两个不同的线程以高频调用 不同 的方法,并且这些调用完美交织,总是使缓存无效。但即使在这种情况下,它可能也不比当前的代码测量得差。

@Vladimir 是的,insntanceMethodCache 可以是最终的。这可能有助于JVM略微提升性能。

@Alex,我提出这个修补程序是因为它目前的情况是一个改进,我当然不认为它将是“最佳解决方案”。在开源精神和逐步进步的精神下,我希望能考虑接受它,即使这个问题保持开放以供将来考虑。这也与 clj-1866 有关,我试图在几个不同的方面改善“快速路径”的反射。如果您更愿意有一个包含大量改进的大型修补程序,我当然可以这样做,但我有一种印象,更小、更“明显”的修补程序会更容易供您审查,但两种方式我都很乐意。

0

评论者:vladimirsitnikov

(链接: ~mikera),你错过了重点。

请查看此处:http://shipilev.net/blog/2014/jmm-pragmatics/#_benchmarks,第77/100页 "SC-DRF: Writers"。

{quote}亚历克谢·希皮廖夫:这强化了这样一个观点,即你最应该避免的是数据共享,而不是易变性{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 的用户。”如果你想要一个新的工单/更改描述,说类似这样的话,我会很乐意这么做,但这感觉很像是官僚主义的框框画画。请将此作为对您的贡献过程的建设性反馈考虑在内。

你具体需要查看哪些基准(即哪些基准)作为这种与性能相关的问题(如本例中所展示的)改进的有效证明?

0

评论者:alexmiller

类似于我在 CLJ-1866 中的评论,这个工单的标题是“Reflector.getMethods 应该被缓存”。这同样是一个解决方案,而不是问题。我想要的是一个像“循环中的重复反射很慢”这样的标题,以及一个从一些示例代码开始的说明,展示了这个问题。在没有良好的问题陈述的情况下,我无法对这个工单进行分类。我可能会考虑问题优先级低到不足以在此时的分类——但我将保留我的判断直到该工单得到改进。

先前变更产生意想不到的性能影响的事实,只让我的建议得到更多支持,即这份(以提高性能为目标的)工单应该验证其主张。您添加了代码,使得该代码的“错过”路径比之前更慢。慢了多少?它应该使“命中”路径更快 —— 快多少?在典型代码中,我们遇到“命中”与“错过”路径的频率是多少?我的假设是示例将证明一个“命中”路径很常见的情况。这些问题是我必须提出的问题,以评估任何提出的解决方案。

此外,您引入了并发问题,还需要进行额外的工作来验证正确性(当前补丁存在可见性问题)以及您没有引入竞争或内存问题。这些问题是任何与缓存相关的优化中典型的,我可以指出许多先前努力解决这些问题的工单。

0

评论者:mikera

感谢Alex解释您的担忧。

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

如果您想要一个以问题为导向的问题,那么我认为为每个“解决方案”创建单独的工单或补丁没有太多意义(尽管一些开源项目选择这样做,它们通常有一个更流畅的流程来处理小的更改或优化,这或许并不适合Clojure的开发流程)

你同意吗?

0

评论者:alexmiller

正如我说的,我们更愿意拥有小型聚焦的工单和补丁,而不是一个大补丁。

我再次强调,如果这是一个类型提示就能解决问题的场景,那么做任何这些工作都没有太多意义。

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