2024 年 Clojure 状况调查!中分享你的想法。

欢迎!请参阅关于页面了解更多关于这是如何工作的信息。

0
Clojure

我读到 Clojure 序列在它们被实现时“缓存”它们的值(例如,在这里 和 这里)。这通常在上讨论使用 transducer 进行值转换相对使用链式序列函数的优点时出现,因为后者方法创建了“中间缓存序列”。

然而,我在处理序列时也遇到过“不要固执己见”的建议,因为保留对序列开头的引用阻止 GC 释放用于保存已看到的序列值的内存。

就我目前对这些想法的理解,它们似乎是相互矛盾的。如果序列缓存它们的成果,那么为什么保留头部很重要呢?缓存难道不意味着所有的实现元素都会保留吗?或者从另一个角度来考虑,如果 GC 可以自由地清理,为什么序列要缓存它们的实现值呢?

我知道我在这件事上忽略了一个明显的错误,但我会非常 appreciate 一个解释。

1 个答案

+2

已选择
 
最佳答案

实现后的序列是内存中对象的一条链,链中的每个节点指向一个值和下一个节点,直到最终指向一个函数对象,这是序列中尚未实现的部分。对实现链中任何节点的引用都会导致从该点开始到链尾的所有节点被强引用,无法被垃圾回收。

如果你只指向链的尚未实现的“末尾”,则你可以收集“你身后”的所有节点。当你“持有头部”时,这个指针指向链的起始处,这会防止从起始点到尚未实现点的N个节点被引用并保存在内存中。

例如

(def r (repeat 100000000 "abcdef"))   ;; r holds a strong reference to the head
(count r)

(count (repeat 100000000 "abcdef"))

可以遍历seq,允许计数器后面的节点被垃圾回收。(注意,range是人们经常用于这类示例的典型东西,但它在典型情况下有优化过的count。)

by
感谢Alex,这一切都很清楚。我仍然不太理解的是,为什么序列被称为“缓存”?如果不存在对先前已实现值的引用(正如你的第二个示例所示),那么在什么场景下会重用先前已实现的值(即缓存命中)?或者这里的“缓存”一词并不打算表示值的重用,只是表示它们在垃圾回收过程中仍然存在于内存中?
by
我相信答案是,这里的缓存意味着如果你确实持有头部,而在第一次遍历序列时,必须计算每个元素,那么当你第二次、第三次等遍历时同时持有头部,就不需要对后续遍历进行重新计算。这些值是通过消耗内存来缓存它们,以避免需要重新计算它们。
by
这里的“缓存”只是意味着链中的节点保留了在该点计算的值。
感谢两位。那么,是否可以说,串联序列函数的潜在性能问题是中间序列会导致更频繁的垃圾回收(GC)来清除已实现的值(在典型情况下,前一个值没有明确定义的引用)?
是的,嵌套序列函数将创建N层节点(相对于transducer减少的1层),这意味着生成的垃圾是N倍,因此收集也是N倍。
...