2024 Clojure状态调查!分享您的想法。

欢迎!请查看关于页面以了解更多关于该功能的信息。

0投票
Clojure

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

然而,我也遇到过处理sequence时的建议“不要抓狂”,因为在sequence中持有头部的引用会阻止GC释放用于保留sequence中之前已见值的内存。

以我当前的理解,这些想法似乎相互矛盾。如果sequence会缓存其结果,那么为什么保存头部很重要?缓存难道不是意味着保留所有已实现元素吗?或者反过来,为什么sequence要缓存实现的值,如果GC可以自由地清理它们呢?

我知道我一定漏掉了什么显而易见的事情,但我非常希望得到一个解释。

1 答案

+2投票

被选中答案
 
最佳答案

一个实现序列是内存中对象链,链中的每个节点指向一个值和下一个节点,直到最终得到一个函数对象,该对象是序列的剩余未实现部分。对实现链中任何节点的引用都会导致从该点开始的其余链被强保留,无法进行 gc(垃圾回收)。

如果您只指向链的未实现“末尾”,则您后面的所有链接都可以被收集。当您“持有头部”时,该指针指向链的起始处,这阻止了从起始到未实现点的 N 个节点保留在内存中。

例如

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

(count (repeat 100000000 "abcdef"))

这可以遍历 seq,允许计数器后面的链接进行 gc。注意,range 是人们常用于此类示例的典型东西,但它在典型情况下有一个优化的 count。

感谢 Alex,这都很有道理。我还是不太明白为什么说 seq 是“缓存的”?如果没有任何对之前实现值的引用(如您的第二个示例所示),那么在什么场景下会重用之前实现的值(即缓存命中)?或者在这里“缓存”这个术语并不是指值的重用,只是这些值仍然保留在内存中,直到 GC 通过吗?
我认为答案是,这里的缓存意味着如果您确实保留了头部,而在对序列的第一次遍历时,每个元素都需要计算,然后您在保留头部的情况下第二次、第三次等遍历它,后面的遍历不再进行重新计算。这些值通过消耗内存来避免需要重新计算它们。
这里的“缓存”只是意味着链中的链接保留了在该点计算的值。
by
感谢两位。那么,可以说,链式 seq 函数的潜在性能问题在于中间序列可能导致更频繁的 GC 会话以清除已实现的值(在典型情况中,之前值没有明确引用)吗?
by
是的,嵌套的序列函数将创建 N 层节点(与 transducer 减少的一层形成对比),这意味着垃圾会增加 N 倍,因此垃圾回收也会增加 N 倍。
...