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

欢迎!请查看关于页面了解有关此功能的一些更多信息。

+13
多态方法

一个简单的重现方式

Clojure 1.10.2
user=> (defmulti f identity)
#'user/f
user=> (defmethod f :default [x] nil)
#object[clojure.lang.MultiFn 0x4bb8855f "clojure.lang.MultiFn@4bb8855f"]
user=> (dotimes [_ 100000] (f (byte-array 1000000)))

Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main"

如果您调整数字以删除OOM错误,您将看到一个持久的泄露。

看起来罪魁祸首是MultiFn.findAndCacheBestMethod缓存了与默认方法匹配的每个单个值。

1 答案

+1

我不知道这是否算得上泄漏,或许仅仅是设计假设启动值数量有界。

曾经我们没有缓存导致:默认情况的结果值,这使得:默认变得非常慢(这曾是多态方法长时间被认为“慢”的主要原因)。在1.8中,http://dev.clojure.org/jira/browse/CLJ-1429进行了这样的改变,这对大量真实程序的性能提升有显著作用。

也许有一些中间地带,不一定。

使用协议避免缓存溢出是否是更合适的选择?或者也许应该手动实现 `case` 并避免使用多方法协议?
我的顾虑并不仅在于开发者错误引起的偶然泄漏,实际上还在于如果允许潜在恶意或高度随机的用户输入触达多方法,可能会造成实际问题。

我相信大多数Clojure程序员都相当谨慎,如果明确指出这种缓存正在进行,脑中会再次思考是否应该有:default方法或找到其他方式来关闭多方法输入 —— 但现在并不明确。我在Clojure上工作了多年,并不知道:default匹配是会被缓存的。

例如,memoize将会引起相同类型的问题,但我觉得在那个情况下(你可能会说很明显),而多方法(multi-methods)对我来说则有点过于微妙。

我不知道是否有解决方案,当然,重要的是要快,在大多数时候,我相信这没问题。
假设这是可能的,defmulti 选项可以使用 clojure.core.cache 中的 LRU 或其他缓存来解决此问题吗?(“慢”的非缓存多方法查找是否比安全边界的缓存开销更大?)
需要注意的是,这种缓存发生在分派函数的输出上,而不是多方法或分派函数的输入上。最常用的分派函数是像 `type`、`class` 或用于从映射式中提取字段的键词等。虽然它们都有未限定的范围,但一般来说,它们的范围比领域更加限定。我在过去5+年的时间里并未遇到其他人报告过这种情况。协议也使用缓存(每个调用点),实际上(除了ns/var表之外),这些是Clojure运行时内两个主要的具有状态的东西。

你可以为此添加很多潜在的控件,非常类似于memoize。在后一种情况下,我们将其扩展到core.memoize/core.cache,并将更简单的版本保持在core中。我并不支持或反对任何一方,需要一些评估。创建了https://clojure.atlassian.net/browse/CLJ-2626
by
当然,最好不要有太多不同的分派值。但有时候分派值可能并不是开发者想要的。

我不小心在在生产环境使用关键字作为分派函数时遇到了内存泄漏。我的意图是根据第一个参数中的字段值进行分派,但错误地当该关键字找不到时,将关键字作为一个函数把多方法函数的第二个参数作为分派值处理。

以下是一个简单的重现例子

```
(defmulti foo :type)
(defmethod foo :default [_ _] nil)
(foo {} (Math/random))
```

我的意图是
```
(defmulti foo (fn [x y]
                (:type x)))
```

注意,我的有缺陷的代码并未导致任何功能上不正确的行为,只是由于多方法的缓存机制引起了内存泄漏。
by
尽管它适用于分派的结果,但结果可以由外部世界或程序提供作为输入,或者除非程序员确保了这一点,否则不能假设它们是有界的。我认为这与协议缓存不同,因为是通过类型而非值来存储的,想象一个通过计数进行分派的多方法,你可以为0、1、2进行特化,然后再留出默认处理其他情况,或者为HTML表单上的:type字段值进行选择,默认可以是不合法选择的备选方案。
仅为了将另一个意外问题添加到可能让人陷入困境的问题列表中。最近,在升级某些依赖项后,一个服务出现了内存泄漏。问题组合是 plumatic schema + ring swagger + compojure api。在这个提交中,Schema 被更改为返回一个匿名函数:https://github.com/plumatic/schema/commit/2191f9e2982da74410c14686ca6c3436e802afc0,且 ring swagger 使用 `identity` 作为调度函数 https://github.com/metosin/ring-swagger/blob/0ef9046174dec0ebd4cbf97d6cc5c6846ef11996/src/ring/swagger/coerce.clj#L178,因此在使用 compojure api 中的查询参数映射时,它最终会将匿名函数放入每项请求的方法缓存中 `ring.swagger.coerce/custom-matcher`(和 `time-matcher`)。我们通过降级 schema 并提高迁移到 spec/malli 的优先级来解决这个问题,但我想加个注释,有些库在野中使用 `identity` 作为多方法调度函数,这似乎是一个我们应该注意的红旗。
我并没有意识到这个问题,ring-swagger 的事是我的责任。我很乐意在那里修复一下。
...