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

欢迎!请参阅关于页面了解更多关于如何使用本页面的信息。

0
Clojure

我曾看到 Clojure 序列在它们的值被实现时“缓存”它们(如此处此处所示)。这通常出现在讨论使用转换器变换值而不是链式序列函数的相对优点时,后者方法会创建“中间缓存序列”。

然而,我也遇到过关于序列处理的建议“不要对你的头抓得太紧”,因为在处理序列时,保持序列开头的引用会阻止 GC 释放用于存储序列中之前 seeing 的值的内存。

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

我知道我肯定忽略了某些明显的方面,但我真的很想得到一个解释。

1答案

+2

被选中
 
最佳答案

实现序列是内存中对象的一个链,其中的每个链接都指向一个值和下一个链接,直到最终到达一个函数对象,这是序列中剩余未实现的部分。对实现链中任何链接的引用都会导致从该点开始的其余链被强引用并无法被 gc'ed。

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

例如

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

(count (repeat 100000000 "abcdef"))

可以遍历 seq,使计数值后面的链接可以 gc'ed。 (注意,range 是人们常用作此类示例的东西,但它具有针对典型情况的优化 count。)

by
感谢 Alex,这一切都很有道理。我仍然不完全懂的是为什么说 seq 是“缓存的”?如果您像您的第二个例子中那样没有引用之前的实现值()那么在什么情况下会重新使用之前实现的值(即缓存命中)?或者是这里的“缓存”术语并不打算暗示值的重用,只是说它们仍然在内存中,直到 GC 传递完成?
by
我认为答案在于,这里的缓存意味着如果你确实保留了头部,那么第一次遍历序列时,每个元素都必须计算,如果你随后再次遍历它而保留头部,那么在后续遍历中无需重新计算。  通过消耗内存来缓存这些值,避免了重新计算它们的需要。
by
这里的“缓存”只是意味着链中的链接保留了在链中该点计算出的值。
by
谢谢两位。那么这么说是否公平:链式序列函数潜在的性能问题在于中间序列会导致更频繁的GC迭代以清除已实现的值(在典型情况中,前一值不是显式引用)?
by
是的,嵌套的序列函数将创建N层节点(相对于转换器的1层),这意味着垃圾是N倍,因此收集也增加N倍。
...