在 Clojure 编程语言中,为什么这段代码能顺利通过?
(let [r (range 1e9)] [(first r) (last r)])
虽然这个失败:
(let [r (range 1e9)] [(last r) (first r)])
我知道这是关于“失去理智”的建议,但请您向我解释一下吗?我还不能消化它。
更新:
很难选择正确的答案,两个答案非常有用。
注意:代码片段来自“The Joy of Clojure”。
在 Clojure 编程语言中,为什么这段代码能顺利通过?
(let [r (range 1e9)] [(first r) (last r)])
虽然这个失败:
(let [r (range 1e9)] [(last r) (first r)])
我知道这是关于“失去理智”的建议,但请您向我解释一下吗?我还不能消化它。
更新:
很难选择正确的答案,两个答案非常有用。
注意:代码片段来自“The Joy of Clojure”。
为了详细说明dfan和Rafał的答案,我花时间使用YourKit分析器运行这两个表达式。
看到 JVM 在工作是很有趣的。第一个程序对 GC 非常友好,以至于 JVM 在管理内存方面真的很出色。
我画了一些图表。
该程序的内存非常低;总体而言,小于 6 兆字节。如前所述,它对 GC 非常友好,它进行了大量的收集,但使用的 CPU 很少。
这个是非常需要内存的。它上升到 300 MB 的 RAM,但这还不够,程序没有完成(JVM 在不到一分钟后就死掉了)。GC 占用高达 90% 的 CPU 时间,这表明它拼命尝试释放它可以释放的任何内存,但找不到任何内存(收集的对象很少甚至没有)。
编辑第二个程序内存不足,触发了堆转储。对此转储的分析表明,70% 的内存是 java.lang.Integer 对象,无法收集。这是另一个屏幕截图:
range
根据需要生成元素。
在 的情况下(let [r (range 1e9)] [(first r) (last r)])
,它抓取第一个元素 (0),然后生成十亿 - 2 个元素,将它们扔掉,然后抓取最后一个元素 (999,999,999)。它永远不需要保留序列的任何部分。
在 的情况下(let [r (range 1e9)] [(last r) (first r)])
,它生成十亿个元素以便能够评估(last r)
,但它还必须保留它生成的列表的开头以便以后评估(first r)
。所以它不能随手扔掉任何东西,而且(我想)内存不足。
真正重要的是序列绑定到r
(不是已经评估的(first r)
,因为你不能从它的值评估整个序列。)
在第一种情况下,当被评估时绑定不再存在(last r)
,因为没有更多的表达式r
需要评估。在第二种情况下,未评估的存在(first r)
意味着评估者需要保持对 的绑定r
。
为了显示差异,计算结果为 OK:
user> (let [r (range 1e8) a 7] [(last r) ((constantly 5) a)])
[99999999 5]
虽然这失败了:
(let [r (range 1e8) a 7] [(last r) ((constantly 5) r)])
尽管后面的表达式(last r)
忽略了r
,但求值器并不那么聪明并保持与 的绑定r
,从而保持整个序列。
编辑:我找到了一个帖子,其中 Rich Hickey 解释了负责清除上述情况下对头部的引用的机制的细节。在这里:当地人清理的 Rich Hickey
有关技术说明,请访问http://clojure.org/lazy。该建议在该部分中提到Don't hang (onto) your head