请在2024 Clojure状态调查!分享您的观点。

欢迎!请阅读关于页面以了解更多信息。

0
Clojure

我听说Clojure序列会在被实现时“缓存”它们的值(例如这里这里)。这通常在讨论使用transducers而不是链式序列函数来转换值时的优劣时出现,因为后者方法会创建"中间缓存的序列"。

然而,我也遇到过“不要对你的头部放手”的说法,当处理序列时,因为持有序列开始的引用会阻止GC释放用于存储序列中先前遇到值的内存。

在我目前的理解水平上,这些观点似乎存在冲突。如果序列缓存它们的结果,那么为什么持有头部很重要呢?缓存不是暗示所有已实现元素都会被保留吗?或者反过来,如果GC可以清除它们,那么为什么序列还会缓存它们的实现值呢?

我知道我肯定遗漏了什么显而易见的东西,但我非常希望得到解释。

1 答案

+2

选择
 
最佳答案

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

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

例如

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

相反

(count (repeat 100000000 "abcdef"))

这可以遍历序列,允许计数器后面的链接进行垃圾回收。(注意,range是人们通常用于这些示例的典型方法,但它具有针对典型情况优化的count。)

感谢Alex,这一切都很有道理。我还是不太明白为什么序列被称为“缓存的”?如果在您的第二个例子中没有对先前实现值的引用(例如),那么在不实现值的场景中,先前实现值是如何被重用的(即缓存命中)?或者这里的“缓存”一词并不意味着值的重用,只是它们仍然被保存在内存中,直到进行垃圾回收周期?
我认为答案是,这里的“缓存”意味着,如果您确实保留了头部,那么在第一次遍历序列时,每个元素都必须被计算,如果您在后继遍历时仍然保留着头部,那么就不需要进行额外的计算。这些值通过消耗内存来缓存,避免了重新计算。
这里的“缓存”只是意味着链中的节点保留了在该点计算的值。
感谢你们二位。那么可以这样说,链式序列函数在潜在的性能问题中,中间序列可能会引起更频繁的GC遍历来清理已实现的值(在典型情况下,前一个值没有被显式引用)吗?
是的,嵌套序列函数将创建N层节点(与transducer还原的一层相比),这意味着垃圾将多出N倍,因此收集的次数也会多出N倍。
...