分享您的观点,参加 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<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周期。

备注。我是一个Java性能工程师,不是一个Clojure工程师(也就是说“我的Clojure知识就在 {{(+ x y)}} 附近”),所以请多多包涵,我没有查看官方文档(RTFM)。
0

评论者:alexmiller

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

缓存听起来像是一个简单的解决方案,直到你考虑到所有的管理方面。缓存是如何清理的?实例是否可变且可重用?是否存在由类加载器或代码重构产生的意外副作用?将共享资源放入调用路径中的并发效果是什么?缓存对内存的影响是什么?它是可配置的吗?

这些都是需要调查的事情,这意味着这并不是一个简单的任务。

0

评论作者:mikera

为 Reflector.getMethods 的简单缓存进行了修补

0

评论作者:mikera

我创建了一个小型修补程序,为 Reflector.getMethods 调用添加了非常简单的(固定大小,每种阶数1个元素)缓存。目的是使这个缓存非常简单,以避免并发效果和可变大小的缓存问题。

在反射调用同一方法的循环中,我的测试中有所改善(大约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"

;; 带有缓存阶数
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,我提出这个补丁是因为它比目前的情况有所改进,我确实不认为它将是“最佳解决方案”。本着开源精神和逐步推进的原则,我希望你能考虑接受它,即使这个问题仍留待未来考虑。这还与clj-1866相关,我正在尝试以几种不同的方式改善反射的“快速路径”。如果你更想有一个含有大量改进的单一大型补丁,我当然可以做到,我有这样的印象,即更小、更“明显”的补丁更容易让你进行审查,但两种方式我都愿意。

0

评论由:vladimirsitnikov 发布

(链接:~mikera),你 missed the point。

请查看这里: http://shipilev.net/blog/2014/jmm-pragmatics/#_benchmarks,幻灯片77/100 "SC-DRF: Writes"。

{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
参考: https://clojure.atlassian.net/browse/CLJ-1784(由 alex+import 报告)
...