懒惰序列还是重复数学幂函数?

时间:2010-05-08 20:55:51

标签: clojure

作为练习,我实现了数学幂函数。一旦使用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"

4 个答案:

答案 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会这样做。

PS2:对于缩进感到抱歉,从Emacs复制粘贴似乎没有保留它...

答案 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)))

您的里程可能会有所不同。

相关问题