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

欢迎!请查看关于页面获取更多关于系统如何工作的小信息。

+1
序列

当随便玩大斐波那契数时,我发现deflet之间存在这种明显的不一致。


user=> (def fibs (fn ([] (fibs 1N 1N)) ([a b] (lazy-seq (cons a (fibs b (+ a b))))))) 'user/fibs user=> (take 20 (fibs))
(1N 1N 2N 3N 5N 8N 13N 21N 34N 55N 89N 144N 233N 377N 610N 987N 1597N 2584N 4181N 6765N)
user=> (def f1m (nth (fibs) 1000000))
OutOfMemoryError Java heap space java.math.BigInteger.add (BigInteger.java:1315)
user=> (def f1m (let [x (nth (fibs) 1000000)] x)) 'user/f1m user=> (take 20 (str f1m))
(\3 \1 \6 \0 \4 \7 \6 \8 \7 \3 \8 \6 \6 \8 \9 \8 \7 \3 \4 \4)
user=> (count (str f1m))
208988

为什么def会消耗内存,而let不会?def是像let那样不持有惰性序列的头结点吗?

1 答案

+2

这实际上是 eval 在 repl 处理“简单”表达式与“复杂”表达式之间的差异。

对于看起来足够简单(像是 def,其中值调用其他函数)的表达式,eval 实际上会像一个解释器操作,而不是发出 JVM 字节码并运行它。这里的解释器代码流实际持有头结点,它比生成的字节码要原始得多。

let 表达式、函数,基本上是任何除了文本值或简单参数的函数调用之外的内容,足够复杂,因此会被编译成字节码,具有更谨慎的参数处理。

"简单表达式"的行为是优化还是必需的?
难以确定,这通常被认为是优化。Clojure最初针对Java 5,其创建了众多小类(如果生成字节码并执行,则必须生活在类中)的成本很高。现在的Java在这方面有所改进。因此,始终生成字节码可能就足够了。

实际上,解释器代码看起来像是在尝试避免持有头部,但某种方式下它还是持有,可能会有某种方法可以修复这个问题。
啊,问题出在nth被内联展开为静态方法调用,而不是函数调用,而解释器所做的反射静态方法调用没有尝试避免持有头部。
...