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式中值调用其他函数的def)的表达式,eval实际上会像解释器一样工作,不会生成jvm字节码并运行。在这里,保留头部的是解释器代码路径,它比生成的字节码更为简单。

let表达式,函数,基本上是除文本值或具有简单参数的函数调用之外的所有内容,都足够复杂,以至于它们会被编译成字节码,字节码对参数的处理更为谨慎。

“简单表达式”的行为是优化还是必须的?
很难说,这通常被描述为一种优化。Clojure最初目标是Java 5,在创建许多小类时存在显著的成本(如果你要生成字节码并执行它,它必须存在于一个类中)。如今,Java在这方面已有所改进。因此,可能只需始终生成字节码即可。

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