20

在 Clojure 编程语言中,为什么这段代码能顺利通过?

(let [r (range 1e9)] [(first r) (last r)])

虽然这个失败:

(let [r (range 1e9)] [(last r) (first r)])

我知道这是关于“失去理智”的建议,但请您向我解释一下吗?我还不能消化它。

更新:
很难选择正确的答案,两个答案非常有用。
注意:代码片段来自“The Joy of Clojure”。

4

4 回答 4

31

为了详细说明dfanRafał的答案,我花时间使用YourKit分析器运行这两个表达式。

看到 JVM 在工作是很有趣的。第一个程序对 GC 非常友好,以至于 JVM 在管理内存方面真的很出色。

我画了一些图表。

GC 友好:(let [r (range 1e9)] [(first r) (last r)])

在此处输入图像描述

该程序的内存非常低;总体而言,小于 6 兆字节。如前所述,它对 GC 非常友好,它进行了大量的收集,但使用的 CPU 很少。

头部持有者:(let [r (range 1e9)] [(last r) (first r)])

在此处输入图像描述

这个是非常需要内存的。它上升到 300 MB 的 RAM,但这还不够,程序没有完成(JVM 在不到一分钟后就死掉了)。GC 占用高达 90% 的 CPU 时间,这表明它拼命尝试释放它可以释放的任何内存,但找不到任何内存(收集的对象很少甚至没有)。

编辑第二个程序内存不足,触发了堆转储。对此转储的分析表明,70% 的内存是 java.lang.Integer 对象,无法收集。这是另一个屏幕截图:

在此处输入图像描述

于 2011-04-18T12:32:27.847 回答
26

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)。所以它不能随手扔掉任何东西,而且(我想)内存不足。

于 2011-04-18T03:15:01.013 回答
12

真正重要的是序列绑定到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

于 2011-04-18T12:19:27.293 回答
2

有关技术说明,请访问http://clojure.org/lazy。该建议在该部分中提到Don't hang (onto) your head

于 2011-04-19T09:56:12.210 回答