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

如果您调整数字以便不会出现内存溢出错误,您将看到一个持续的泄漏。

罪魁祸首似乎是MultiFn.findAndCacheBestMethod为与默认方法匹配的每个值缓存。

1 答案

+1

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

在一段时间内,我们没有为产生:default情况的分派值进行缓存,这使得:default的速度非常慢(这长期以来是认为多方法是“慢”的的主要原因之一)。在1.8中,http://dev.clojure.org/jira/browse/CLJ-1429改变了这一点,并为大量真实程序提供了显著的性能提升。

也许有一种妥协的方案,但我并不确定。

对于这种情况,使用协议是否是避免缓存溢出的更好替代方案?或者也许仅仅手动实现一个`case`,从而避免使用多方法和协议?
by
我担心的是,不仅仅是开发者错误导致的不小心泄露,如果(潜在有害或高度随机的)用户输入被允许命中多方法,这可能会造成真正的麻烦。

我相信大多数Clojure程序员都非常谨慎,如果明确说明这种缓存正在进行,人们的思维就会再次考虑是否有默认方法,或者以其他方式关闭多方法的输入——但目前这并不清晰。我已经多年来专业使用Clojure,并不知道默认匹配件会被缓存。

例如,memoize会导致类似的问题,但在我看来,在那个情况下(你可能说是明显的)比多方法稍微微妙一点。

我不知道是否有解决方案,当然快速执行很重要,而且在几乎所有情况下,我相信这都没问题。
by
假设这是可能的,定义multimethod选项以使用clojure.core.cache中的LRU或其他缓存是否可以解决这个问题?(是"慢"的不缓存的多方法查找比安全界限缓存的开销慢吗?)
by
需要注意的是,这种缓存发生在调度函数的输出上,而不是在多方法或调度函数的输入上。最常见的调度函数比如`type`、`class`或用于从映射中提取字段的keyword。虽然它们都具有无界的范围,但它们通常比领域更受控制。我不记得有人在5年多之前的更改后报告过这一点。协议也使用缓存(每个调用点)实际上(除了ns/var表之外)这些是Clojure运行时中两个主要的有状态对象。

您可以添加很多控件以达到这一目的,这与memoize非常相似。在后面的案例中,我们将核心.memoize/core.cache推向核心,并保持了核心中的简单版本。我并不是在论证对/错,这将需要一些评估。我已经创建https://clojure.atlassian.net/browse/CLJ-2626
完全没有必要有太多不同的分发值。但有时分发值并不是开发者想要的那样。

在生产环境中,我不小心使用关键字作为分发函数时遇到了内存泄漏。我的本意是在第一个参数关联的字段值上进行分发,但不幸的是,当关键字未找到时,关键字作为函数将多方法中的第二个参数视为分发值。

以下是一个简单的复现示例:

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

我本想得到以下结果:
```
(defmulti foo (fn [x y]
                (:type x)))
```

注意,我的有缺陷的代码并没有造成任何功能上的问题,只因为多方法缓存的机制导致了内存泄漏。
即使在分发结果中适用,这些结果也可以从外部世界或其他程序提供,除非程序员确保这种情况成立,否则无法假定其是有界的。我认为这与协议缓存有很大不同,因为类型和值的区别,想象一个根据count进行分发的多方法,你可以为0,1,2特殊情况化,然后让:default处理其他情况,或者是HTML表单上选择的值,:default可以作为无效选择的回退。
只是为了向可能让人们感到困惑的意外问题列表中添加另一个案例。最近服务中出现了内存泄漏,原因是升级了一些依赖项。问题组合是plumatic schema + ring swagger + compojure api。在这次提交中,schema改为返回匿名函数:[https://github.com/plumatic/schema/commit/2191f9e2982da74410c14686ca6c3436e802afc0](https://github.com/plumatic/schema/commit/2191f9e2982da74410c14686ca6c3436e802afc0),而ring swagger使用 `identity` 作为分派函数 [https://github.com/metosin/ring-swagger/blob/0ef9046174dec0ebd4cbf97d6cc5c6846ef11996/src/ring/swagger/coerce.clj#L178](https://github.com/metosin/ring-swagger/blob/0ef9046174dec0ebd4cbf97d6cc5c6846ef11996/src/ring/swagger/coerce.clj#L178) 所以在cojure api调用中使用查询参数中的映射时,最终将在每个请求中将匿名函数放入`ring.swagger.coerce/custom-matcher`(以及`time-matcher`)的方法缓存中。我们通过降级schema并提高迁移到spec/malli的优先级来解决这个问题,但我只是想添加一个备注,指出有些库使用`identity`作为多方法分派函数,这似乎是一个我们应该注意的红旗。
我对此一无所知,并且ring-swagger是我的责任。我很愿意在那里进行修复。
...