请分享您的想法,参加 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 循环。

PS. 我是一名 Java 性能工程师,而不是 Clojure 工程师(也就是说,“我的 Clojure 知识大概在 {{(+ x y)}} 附近”),所以请多多见谅我没有阅读手册。
0

由 alexmiller 发表的评论

不,我想说,如果反射是你的应用程序的热点,通常值得花几分钟在那些热点区域添加类型提示,这是 Clojure 应用的常见建议。一旦完成这项最少的工作,很少有 Clojure 应用会因反射而受约束。

caching 看起来像是一个简单的解决方案,直到你考虑所有的管理方面。缓存怎么清理的?实例是否可变并能重复使用?在类加载器或代码重新加载的情况下是否存在意外的副作用?将共享资源放入调用路径的并发影响是什么?缓存的内存影响是什么?它是否可配置?

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

0

评论者:mikera

修复程序,用于对 Reflector.getMethods 调用进行简单的缓存

0

评论者:mikera

我创建了一个小的修复程序,为 Reflector.getMethods 调用添加了非常简单的(固定大小,每个算术表达式一个元素)缓存。目的是保持它非常简单,以避免并发影响和可变大小缓存等问题。

这在我对在循环中调用相同方法进行反射的测试中帮助不大(大约 15-20%),这可能是人们真正关心反射性能的典型场景。

由于我认为大部分开销实际上是在 invokeMatchingMethod 上,所以性能还可以进一步提高。但这是一个独立的议题。此修复程序为该领域进一步性能优化开启了道路。

;; clojure 1.8.0-RC3
user=> (let ((v (identity 1)) (time (dotimes (i 1000) (.doubleValue v))))
"已用时间:1.598779 毫秒"

;; 使用缓存的算术表达式
user=> (let ((v (identity 1)) (time (dotimes (i 1000) (.doubleValue v))))
"已用时间:1.359888 毫秒"

0

评论者:vladimirsitnikov

(link: ~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 是的,insntanceMethodCache可以声明为final。我猜这可能会帮助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报告)
...