Clojure 递归函数中的大型绑定是否会影响性能?

Do large bindings inside a Clojure recursive function harm performance?

我正在编写一个函数,它根据一组声明性规则执行一组转换。这些规则是一个编译时评估的集合(没有函数调用或任何东西)。它们将包含数百个元素。

基本布局如下所示:

(defn dostuff-with-rules [stuff]
  (let [rules [["foo"] ["bar"] ["baz"] ...]
        transformed (reduce apply-rule stuff rules)]
    (if (not= stuff transformed)
      (recur transformed)
      transformed)))) 

我担心为每个函数调用初始化大量数据会损害性能,最好将 rules 移到函数范围之外。

这是否有意义,或者 Clojure 是否足够聪明,只需初始化一次规则?或者在 let 绑定中放置 loop 是否有意义?

编辑: 如果这不是简单的尾递归而是树遍历,对每个节点递归调用 dostuff-with-rules 怎么办?

Clojure(或者更确切地说是编译器和 JVM 的组合)确实足够聪明,不会在循环和重复出现时重复分配常量,因此 运行 出现问题的风险很小。如果你有一个昂贵的函数来初始化规则并将其放在 loop/fn/recur 中,那么它确实会成为一个问题,尽管它很容易修复。

这是一个每次都重新计算向量的示例:

user> (time (loop [a 1]
              (if (< a 4)
                (let [big (vec (range 10e6))]
                  (do (println (rand-nth big))
                      (recur (inc a)))))))
9528975
717854
729682
"Elapsed time: 3753.978349 msecs"
nil

引用常量的地方:

user> (def big (vec (range 10e6)))
#'user/big
user> (time (loop [a 1]
              (if (< a 4)
                (do (println (rand-nth big))
                    (recur (inc a))))))
4002962
7528467
2596236
"Elapsed time: 0.685522 msecs"
nil

因此,如果您将规则置于常量中,例如从配置文件中加载它们,那么您将获得快速的性能和有意义的管理方式。

如果您尝试使用太大的文字值,它确实会失败(在这种情况下,我在宏中生成它)

user> (defmacro make-big-vec [] (vec (range 10000)))
#'user/make-big-vec
user> (time (loop [a 1]
              (if (< a 4)
                (let [big (make-big-vec)]
                  (do (println (rand-nth big))
                      (recur (inc a)))))))
CompilerException java.lang.RuntimeException: Method code too large!, compiling:(/tmp/form-init1716519094506420012.clj:1:7) 

虽然 1000 可以正常工作。