作为练习,我实现了数学幂函数。一旦使用recur:
(defn power [a n]
(let [multiply (fn [x factor i]
(if (zero? i)
x
(recur (* x factor) factor (dec i))))]
(multiply a a (dec n))))
user=> (time (dotimes [_ 10000] (power 2 512)))
"Elapsed time: 1839.406746 msecs"
一次使用lazy-seq:
(defn power [a n]
(letfn [(multiply [a factor]
(lazy-seq
(cons a (multiply (* a factor) factor))))]
(nth (multiply a a) (dec n))))
user=> (time (dotimes [_ 10000] (power 2 512)))
"Elapsed time: 2162.297827 msecs"
您认为哪种实施更优越?我真的不知道..(我会使用复发,因为它更容易理解。)
我读到lazy-seq很快,因为它使用内部缓存。但我在样本中看不到缓存的任何机会。我忽略了什么吗?
更新的
我发布了样品的时间。似乎复发在这里稍快一些。
常规递归也不会太糟糕:
(defn power [a n]
(if (== n 1)
a
(* a (power a (dec n)))))
user=> (time (dotimes [_ 10000] (power 2 512)))
"Elapsed time: 1877.34521 msecs"
答案 0 :(得分:6)
首先,通常的建议是首先选择the correct algorithm,稍后担心实现细节(如果您的代码实际上对性能敏感或者可能在上下文中使用)。
然后有审美考虑因素。 recur
对我来说似乎更干净,因为这是解决问题的完美自然方式。当他们以某种方式输入语义图片时使用序列是有意义的,否则,使代码更容易编写/理解/提高性能。这里没有这种情况。
最后,我绝对希望recur
总体上更快,只是因为它避免了不必要的分配& GC。最初的时间支持这一点。确实没有机会从这里的任何类型的缓存中受益,因为无论何时调用power
都会从头开始生成序列,并且在返回后永远不会保留它。
答案 1 :(得分:3)
我在这里提供了一些我自己的懒函数函数,以展示如何从函数中重用lazy-seq。
每当你使用两个数字调用my-simple-lazy-power
时,它会构建一个具有特定数字x的幂的懒惰seq并返回它的第n项。使用这个版本非常昂贵,因为它为每个函数调用构造了一个lazy-seq。这可能就是为什么我的简单懒惰力量的基准测试非常慢。由于lazy-seqs缓存其结果,您可能希望重用它们。这就是my-lazy-power
所做的:它为数字x构造一个lazy-seq并在其周围包含一个函数,该函数接受n作为参数。您可以重复使用后一个函数来访问缓存的结果。 (只要函数存在,该函数就会保持对lazy-seq的引用,因为它'关闭'lazy-seq。这就是为什么它们将它称为闭包。)
缓存函数结果的另一种常用方法是使用函数的memoized版本。基本上memoize
会记住您传入的参数的结果,因此下次传入完全相同的参数时,它将从缓存中返回结果。见下面的例子。为了比较,我还为您的版本和它们的备忘版本定时。
(defn my-simple-lazy-power [x n]
(let [my-lazy-list
((fn my-lazy [y]
(lazy-cat [y] (map #(* % x) (my-lazy y)))) x)]
(nth my-lazy-list n)))
(defn my-lazy-power [x]
(let [my-lazy-list
((fn my-lazy [y]
(lazy-cat [y] (map #(* % x) (my-lazy y)))) x)]
(fn [n]
(nth my-lazy-list n))))
(defn rec-power [a n]
(let [multiply (fn [x factor i]
(if (zero? i)
x
(recur (* x factor) factor (dec i))))]
(multiply a a (dec n))))
(defn lazy-power [a n]
(letfn [(multiply [a factor]
(lazy-seq
(cons a (multiply (* a factor) factor))))]
(nth (multiply a a) (dec n))))
(def mem-my-simple-power (memoize my-simple-lazy-power))
(def mem-my-power (memoize my-lazy-power))
(def mem-rec-power (memoize rec-power))
(def mem-laz-power (memoize lazy-power))
(time (dotimes [_ 50] (my-simple-lazy-power 2 512)))
"Elapsed time: 7138.346976 msecs"
nil
(time (let [my-pow-2 (my-lazy-power 2)]
(dotimes [_ 10000] (my-pow-2 512))))
"Elapsed time: 854.717301 msecs"
nil
(time (dotimes [_ 10000] (rec-power 2 512)))
"Elapsed time: 2726.559879 msecs"
nil
(time (dotimes [_ 10000] (mem-rec-power 2 512)))
"Elapsed time: 4.775677 msecs"
nil
(time (dotimes [_ 10000] (lazy-power 2 512)))
"Elapsed time: 3617.100209 msecs"
nil
(time (dotimes [_ 10000] (mem-laz-power 2 512)))
"Elapsed time: 4.95887 msecs"
nil
PS:我必须在我的版本中围绕lazy-seq定义编写fn
,因为let不支持递归定义,但fn
会这样做。
答案 2 :(得分:1)
您应该进行计时测试,同时运行1m并查看时间。通常非递归函数更快,但在处理函数语言时,递归是首选方法,因为它们通常使用尾调用。基于Java Clr的Clojure所以我现在不支持尾调用,但如果是,它应该和非递归调用一样快。
答案 3 :(得分:1)
加入Michael Marczyck的答案......
您可以将multiply
函数的定义和调用折叠为loop
:
(defn power [a n]
(loop [x a, factor a, i (dec n)]
(if (zero? i)
x
(recur (* x factor) factor (dec i)))))
...但它运行得不快。
正如MM所写,选择正确的算法很重要。他建议的那个例子大约是你的例子的二十倍:
(defn power [x n]
(loop [acc 1, n n, factor x]
(if (zero? n)
acc
(recur
(if (even? n) acc (* acc factor))
(quot n 2)
(* factor factor)))))
你必须提示当前的Clojure使用BigInt
,否则你会得到整数溢出:
(time (dotimes [_ 10000] (power 2N 512)))
您的里程可能会有所不同。