如何在 Clojure 中忘记惰性序列的 head(GC'd)?
How to forget head(GC'd) for lazy-sequences in Clojure?
假设我有一个 巨大的 lazy seq
并且我想对其进行迭代,以便我可以处理在迭代过程中获得的数据。
问题是我想 失去 lazy seq
(已处理)的头(GC'd),这样我就可以处理具有 数百万数据 而没有 OutofMemoryException
.
我有 3 个例子 我不确定。
您能否为此目的提供最佳实践(示例)?
这些函数是不是掉头了?
示例 1
(defn lose-head-fn
[lazy-seq-coll]
(when (seq (take 1 lazy-seq-coll))
(do
;;do some processing...(take 10000 lazy-seq-coll)
(recur (drop 10000 lazy-seq-coll)))))
示例 2
(defn lose-head-fn
[lazy-seq-coll]
(loop [i lazy-seq-coll]
(when (seq (take 1 i))
(do
;;do some processing...(take 10000 i)
(recur (drop 10000 i))))))
示例 3
(doseq [i lazy-seq-coll]
;;do some processing...
)
更新: 这个答案里也有解释 here
复制我上面的评论
据我所知,以上所有都会失去头部(前两个很明显,因为你手动放下头部,而 doseq
的文档声称它不会保留头部)。
这意味着如果您传递给函数的 lazy-seq-coll
未与 def
或 let
绑定到其他地方并在以后使用,则无需担心。所以 (lose-head-fn (range))
不会吃掉你所有的内存,而
(def r (range))
(lose-head-fn r)
可能会。
我能想到的唯一最佳实践是不要 def
可能无限(或只是巨大的)序列,因为它们所有已实现的项目将永远存在于变量中。
一般来说,您必须注意不要在本地或全局保留对惰性序列的一部分的引用,该部分先于另一个涉及过多计算的惰性序列。
例如:
(let [nums (range)
first-ten (take 10 nums)]
(+ (last first-ten) (nth nums 100000000)))
=> 100000009
这在现代机器上大约需要 2 秒。这个怎么样?不同之处在于最后一行,其中交换了 +
的参数顺序:
;; Don't do this!
(let [nums (range)
first-ten (take 10 nums)]
(+ (nth nums 100000000) (last first-ten)))
你会听到你的 chassis/cpu 粉丝活跃起来,如果你是 运行 htop
或类似的人,你会看到内存使用量增长得相当快(大约 1G在最初的几秒钟对我来说)。
怎么回事?
与链表非常相似,clojure 中惰性序列中的元素引用序列中接下来的部分。在上面的第二个例子中,+
的第二个参数需要 first-ten
。因此,即使 nth
乐于不保留对任何内容的引用(毕竟,它只是在长列表中查找索引),first-ten
指的是序列的一部分,如上所述,必须保留对序列其余部分的引用。
相比之下,第一个示例计算 (last first-ten)
,此后,不再使用 first-ten
。现在对惰性序列的任何部分的唯一引用是 nums
。当 nth
完成其工作时,不再需要它完成的列表的每个部分,并且由于没有其他内容引用此块中的列表,如 nth
遍历链表,被检查过的序列占用的内存可以被垃圾回收。
考虑一下:
;; Don't do this!
(let [nums (range)]
(time (nth nums 1e8))
(time (nth nums 1e8)))
为什么这与上面的第二个例子有相似的结果?因为序列将在第一次实现时(第一个 (time (nth nums 1e8))
)被缓存(保存在内存中),因为在下一行 上使用了 nums。相反,如果我们对第二个 nth
使用 不同的 序列,则无需缓存第一个,因此可以在处理时将其丢弃:
(let [nums (range)]
(time (nth nums 1e8))
(time (nth (range) 1e8)))
"Elapsed time: 2127.814253 msecs"
"Elapsed time: 2042.608043 msecs"
因此,当您使用大型惰性序列时,请考虑是否有任何内容仍指向该列表,如果有任何内容(全局变量是常见的),那么它将 保存在内存中。
假设我有一个 巨大的 lazy seq
并且我想对其进行迭代,以便我可以处理在迭代过程中获得的数据。
问题是我想 失去 lazy seq
(已处理)的头(GC'd),这样我就可以处理具有 数百万数据 而没有 OutofMemoryException
.
我有 3 个例子 我不确定。
您能否为此目的提供最佳实践(示例)?
这些函数是不是掉头了?
示例 1
(defn lose-head-fn
[lazy-seq-coll]
(when (seq (take 1 lazy-seq-coll))
(do
;;do some processing...(take 10000 lazy-seq-coll)
(recur (drop 10000 lazy-seq-coll)))))
示例 2
(defn lose-head-fn
[lazy-seq-coll]
(loop [i lazy-seq-coll]
(when (seq (take 1 i))
(do
;;do some processing...(take 10000 i)
(recur (drop 10000 i))))))
示例 3
(doseq [i lazy-seq-coll]
;;do some processing...
)
更新: 这个答案里也有解释 here
复制我上面的评论
据我所知,以上所有都会失去头部(前两个很明显,因为你手动放下头部,而 doseq
的文档声称它不会保留头部)。
这意味着如果您传递给函数的 lazy-seq-coll
未与 def
或 let
绑定到其他地方并在以后使用,则无需担心。所以 (lose-head-fn (range))
不会吃掉你所有的内存,而
(def r (range))
(lose-head-fn r)
可能会。
我能想到的唯一最佳实践是不要 def
可能无限(或只是巨大的)序列,因为它们所有已实现的项目将永远存在于变量中。
一般来说,您必须注意不要在本地或全局保留对惰性序列的一部分的引用,该部分先于另一个涉及过多计算的惰性序列。
例如:
(let [nums (range)
first-ten (take 10 nums)]
(+ (last first-ten) (nth nums 100000000)))
=> 100000009
这在现代机器上大约需要 2 秒。这个怎么样?不同之处在于最后一行,其中交换了 +
的参数顺序:
;; Don't do this!
(let [nums (range)
first-ten (take 10 nums)]
(+ (nth nums 100000000) (last first-ten)))
你会听到你的 chassis/cpu 粉丝活跃起来,如果你是 运行 htop
或类似的人,你会看到内存使用量增长得相当快(大约 1G在最初的几秒钟对我来说)。
怎么回事?
与链表非常相似,clojure 中惰性序列中的元素引用序列中接下来的部分。在上面的第二个例子中,+
的第二个参数需要 first-ten
。因此,即使 nth
乐于不保留对任何内容的引用(毕竟,它只是在长列表中查找索引),first-ten
指的是序列的一部分,如上所述,必须保留对序列其余部分的引用。
相比之下,第一个示例计算 (last first-ten)
,此后,不再使用 first-ten
。现在对惰性序列的任何部分的唯一引用是 nums
。当 nth
完成其工作时,不再需要它完成的列表的每个部分,并且由于没有其他内容引用此块中的列表,如 nth
遍历链表,被检查过的序列占用的内存可以被垃圾回收。
考虑一下:
;; Don't do this!
(let [nums (range)]
(time (nth nums 1e8))
(time (nth nums 1e8)))
为什么这与上面的第二个例子有相似的结果?因为序列将在第一次实现时(第一个 (time (nth nums 1e8))
)被缓存(保存在内存中),因为在下一行 上使用了 nums。相反,如果我们对第二个 nth
使用 不同的 序列,则无需缓存第一个,因此可以在处理时将其丢弃:
(let [nums (range)]
(time (nth nums 1e8))
(time (nth (range) 1e8)))
"Elapsed time: 2127.814253 msecs"
"Elapsed time: 2042.608043 msecs"
因此,当您使用大型惰性序列时,请考虑是否有任何内容仍指向该列表,如果有任何内容(全局变量是常见的),那么它将 保存在内存中。