gpt4 book ai didi

Clojure 头部保留

转载 作者:太空宇宙 更新时间:2023-11-03 18:32:13 25 4
gpt4 key购买 nike

我正在阅读 O'Reilly 的 Clojure Programming 一书..

我遇到了一个头部保留的例子。第一个示例保留对 d 的引用(我想),因此它不会被垃圾收集:

(let [[t d] (split-with #(< % 12) (range 1e8))]
[(count d) (count t)])
;= #<OutOfMemoryError java.lang.OutOfMemoryError: Java heap space>

虽然第二个例子没有保留它,所以它没有问题:

(let [[t d] (split-with #(< % 12) (range 1e8))]
[(count t) (count d)])
;= [12 99999988]

我在这里没有得到的是在哪种情况下究竟保留了什么以及为什么。如果我尝试只返回 [(count d)],像这样:

(let [[t d] (split-with #(< % 12) (range 1e8))]
[(count d)])

它似乎会造成同样的内存问题。

此外,我记得读过 count 在每种情况下都实现/评估一个序列。所以,我需要澄清一下。

如果我首先尝试返回 (count t),与我根本不返回它相比,它的速度/内存效率如何?什么以及为什么在这种情况下被保留?

最佳答案

在第一个和最后一个示例中,传递给 split-with 的原始序列在内存中完整实现的同时被保留;因此 OOME。这种情况发生的方式是间接的;直接保留的是 t,而原始序列由 t 保留,这是一个惰性序列,处于未实现状态。 p>

t 使原始序列保持不变的方式如下。在实现之前,t 是一个 LazySeq 对象,存储一个 thunk,它可能在某些时候被调用以实现 t;这个 thunk 需要在实现将其传递给 take-while 之前将指向原始序列参数的指针存储到 split-with -- 查看 的实现>拆分。一旦 t 被实现,thunk 就可以在 t 不再持有巨大输入序列的头部。

输入序列本身完全由(count d)实现,它需要实现d,因此原始输入序列。

继续讨论为什么要保留 t:

在第一种情况下,这是因为 (count d)(count t) 之前被求值。由于 Clojure 从左到右计算这些表达式,本地 t 需要在第二次调用 count 时停留,并且由于它恰好持有一个巨大的 seq(如上所述),这导致OOME。

最后一个只返回 (count d) 的例子最好不要保留 t;不是这种情况的原因有些微妙,最好通过引用第二个示例来解释。

第二个例子恰好工作正常,因为在 (count t) 被求值后,不再需要 t 了。 Clojure 编译器注意到这一点,并使用一个巧妙的技巧在进行 count 调用的同时将本地重置为 nil。关键的 Java 代码片段做了类似 f(t, t=null) 的事情,以便将 t 的当前值传递给适当的函数,但局部是在控制权移交给 f 之前清除,因为这是表达式 t=null 的副作用,它是 f 的参数;很明显,Java 的从左到右语义是这项工作的关键。

回到最后一个例子,这是行不通的,因为 t 实际上并没有在任何地方使用,而且未使用的局部变量不会被局部变量清除过程处理。 (清除发生在最后一次使用时;如果程序中没有这样的点,则没有清除。)

至于 count 实现惰性序列:它必须这样做,因为没有通用的方法可以在不意识到的情况下预测惰性序列的长度。

关于Clojure 头部保留,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/15994316/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com