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

欢迎!请查阅关于页面以获取有关此工作的更多信息。

+1投票
序列

在随机玩大斐波那契数时,我发现了def和let之间似乎不一致的问题。


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被内联扩展为静态方法调用而不是函数调用,而解释器进行的反射静态方法调用并没有尝试避免持有头。
...