请在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

我不知道这是否更多的是一个设计假设,即分派值数量有界。

一度我们没有缓存产生`:default`情况下的分派值,这使得`:default`非常慢(这是多方法多年来被认为“慢”的主要原因)。在1.8中,http://dev.clojure.org/jira/browse/CLJ-1429做出了这一改变,并为许多实际程序带来了显著性能提升。

可能存在某种中间方案,不确定。

对于这种情况,使用协议是否是比避免缓存溢出的更好选择?或者只是手动实现一个`case`并避免使用多方法和协议?
by
我的担忧不是由于开发者错误导致的事故性内存泄漏,而是实际上,如果允许(可能是恶意的或高度随机的)用户输入击中多重方法,可能会引起实际问题。

我相信大多数Clojure程序员都非常小心,如果明确说明正在执行此缓存,则人们会再次考虑是否有必要具有:默认方法或以其他方式关闭对多重方法的输入,但这种说法并不明确。我已经在专业上使用Clojure多年,但我不知道:default匹配项会被缓存。

例如,memoize将导致同样的问题,但我认为在这种情况下更清晰(你可能认为这是显而易见的),而多重方法对我而言则有一点微妙。

当然,速度快很重要,而且我相信在大多数时候,这都没问题。
by
假设这是可能的,使用clojure.core.cache的LRU或其他缓存作为defmulti选项来解决该问题吗?(“慢”的未缓存的multiplimethod查找是否比安全边界缓存的开销更大?)
需要注意的是,此缓存发生在调度函数的输出上,而不是在多重方法或调度函数的输入上。最常见的调度函数是类似 `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
即便它适用于分发的结果,这些结果也可以作为外部世界或其他程序的输入提供,除非程序员已经确保情况是这样的。我确实认为这与协议缓存有很大不同,因为类型与值的比较,想象一下,如果一个多方法根据count分发,你可以为0、1、2特殊化,然后让:default处理其他情况,或者一个:type字段,其值是在HTML表单中选择的,:default可以作为无效选择的后备。
by
仅为此列表中的意外问题再添加一个案例。最近,由于升级了一些依赖项,一个服务中出现了内存泄漏。问题组合是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 因此,当在coojure api调用中使用查询参数中的map时,它最终会在每次请求中将匿名函数放入`ring.swagger.coerce/custom-matcher`(以及`time-matcher`)的方法缓存中。我们通过降级schema并提高迁移到spec/malli的优先级解决了我们的问题,但我只是想加上一条笔记,有些库在野外使用`identity`作为一个多方法分发函数,这似乎是一个我们应该注意的红旗。
我不了解这一点,ring-swagger是我的行为。我很乐意在那里修理一下。
...