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

欢迎!请查阅关于页面了解更多关于如何使用此工具的信息。

0
Clojure

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

然而,我也遇到了关于处理序列时的建议“不要死盯着你的头”,因为保留对序列开头的引用会阻止垃圾收集(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。)

by
谢谢Alex,这些都很有道理。我仍不太明白为什么序列会被说成是“缓存”的?如果没有任何对先前实现值的引用(就像你的第二个例子一样),那么在什么情况下会重用先前实现的值(即命中缓存)?或者,这里的“缓存”并不是指值的重用,只是它们在垃圾回收之前仍在内存中吗?
by
我认为答案是这里的缓存意味着如果你确实持有头部,而在第一次遍历序列时,每个元素都必须计算,如果你然后在保持头部的同时再次、第三次等遍历它,就不会在这些后续遍历中进行重新计算。值通过消耗内存来缓存,从而避免了重新计算的需要。
by
这里的“缓存”只是意味着链中的链节保留了链上该点的计算值。
by
感谢各位。那么是否可以说,链式序列函数的潜在性能问题在于中间序列可能引发更频繁的垃圾回收机制,以清除已实现的值(在通常情况下,前一个值未被显式引用)?
by
是的,嵌套序列函数将创建N层节点(与转置的1层节点相比),这意味着要产生N倍的垃圾,从而需要N倍的回收。
...