Clojure递归函数内部的大型绑定是否会损害性能?
问题描述:
我在写一个函数,它根据一组声明规则进行一组转换。规则是一个编译时评估的集合(无函数调用或任何其他)。它们将包含数百个元素。Clojure递归函数内部的大型绑定是否会损害性能?
基本布局是这样的:
(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工作正常。