为什么我的Clojure素数懒惰序列如此之慢?

时间:2016-01-18 05:36:59

标签: clojure

我正在做Euler项目的问题7(计算第10001个素数)。我已经以懒惰序列的形式编写了一个解决方案,但是它非常慢,而我在网络上找到的另一个解决方案(下面的链接)和基本相同的解决方案只需不到一秒钟。

我是clojure和懒惰序列的新手,所以我对吃东西,懒猫休息或地图的使用可能是罪魁祸首。你能看看我的代码并告诉我你是否看到了什么?

在一秒钟内运行的解决方案在这里: https://zach.se/project-euler-solutions/7/

它不使用延迟序列。我想知道为什么它如此之快,而我的速度却很慢(他们遵循的过程类似)。

我的解决方案超级慢:

(def primes 
  (letfn [(getnextprime [largestprimesofar]
    (let [primessofar (concat (take-while #(not= largestprimesofar %) primes) [largestprimesofar])]
      (loop [n (+ (last primessofar) 2)]
          (if
            (loop [primessofarnottriedyet (rest primessofar)]
              (if (= 0 (count primessofarnottriedyet))
                true
                (if (= 0 (rem n (first primessofarnottriedyet)))
                  false
                  (recur (rest primessofarnottriedyet)))))
            n
            (recur (+ n 2))))))]
    (lazy-cat '(2 3) (map getnextprime (rest primes)))))

要尝试它,只需加载它并运行类似(取10000个素数),但使用Ctrl + C来终止进程,因为它太慢了。但是,如果你尝试(拿100个素数),你应该立即得到答案。

2 个答案:

答案 0 :(得分:2)

让我重新编写您的代码,将其分解为更易于讨论的部分。我正在使用相同的算法,我只是将一些内部形式拆分成单独的函数。

(declare primes)   ;; declare this up front so we can refer to it below

(defn is-relatively-prime? [n candidates]
  (if (= 0 (count candidates))
    true
    (if (zero? (rem n (first candidates)))
      false
      (is-relatively-prime? n (rest candidates)))))

(defn get-next-prime [largest-prime-so-far]
  (let [primes-so-far (concat (take-while #(not= largest-prime-so-far %) primes) [largest-prime-so-far])]
    (loop [n (+ (last primes-so-far) 2)]
      (if
        (is-relatively-prime? n (rest primes-so-far))
        n
        (recur (+ n 2))))))

(def primes
  (lazy-cat '(2 3) (map get-next-prime (rest primes))))

(time (let [p (doall (take 200 primes))]))

最后一行只是为了更容易在REPL中获得一些非常粗略的基准测试。通过将定时语句作为源文件的一部分,我可以继续重新加载源,并且每次都获得一个新的基准。如果我只加载文件一次,并继续尝试执行(take 500 primes),则基准将会出现偏差,因为primes将保留已经计算过的素数。我还需要doall因为我在let语句中提取素数,如果我不使用doall,它只会将惰性序列存储在{{1}中而不是实际计算素数。

现在,让我们得到一些基本值。在我的电脑上,我明白了:

p

所以大约275毫秒,给予或采取50.我的第一个怀疑是我们如何在Loading src/scratch_clojure/core.clj... done "Elapsed time: 274.492597 msecs" Loading src/scratch_clojure/core.clj... done "Elapsed time: 293.673962 msecs" Loading src/scratch_clojure/core.clj... done "Elapsed time: 322.035034 msecs" Loading src/scratch_clojure/core.clj... done "Elapsed time: 285.29596 msecs" Loading src/scratch_clojure/core.clj... done "Elapsed time: 224.311828 msecs" primes-so-far语句中获得let。我们正在遍历完整的素数列表(据我们所知),直到我们得到一个等于迄今为止最大素数的素数。然而,我们构造代码的方式,所有素数都已按顺序排列,所以我们实际上是通过除最后一个之外的所有素数,然后连接最后一个值。我们最终得到的值与get-next-prime序列中迄今为止实现的值完全相同,因此我们可以跳过整个步骤,只使用primes。这应该可以为我们节省一些东西。

我的下一个怀疑是在循环中调用primes。当我们在一个序列上使用(last primes-so-far)函数时,它也会将列表从头部向下移动到尾部(或者至少,这是我的理解 - 我不会把它从Clojure编译器编写者那里偷走了在一些特殊情况下的代码可以加快速度。)但是,我们并不需要它。我们用last调用get-next-prime,因为我们的素数是有序的,就我们已经意识到的那样,这已经是最后一个素数,所以我们可以使用largest-prime-so-far而不是largest-prime-so-far。这将给我们这个:

(last primes)

这似乎应该加快速度,因为我们在素数序列中消除了两次完整的步行。我们来试试吧。

(defn get-next-prime [largest-prime-so-far]
  ; deleted the let statement since we don't need it
  (loop [n (+ largest-prime-so-far 2)]
    (if
      (is-relatively-prime? n (rest primes))
      n
      (recur (+ n 2)))))
嗯,也许稍好一点(?),但不是我预期的改善。让我们看一下Loading src/scratch_clojure/core.clj... done "Elapsed time: 242.130691 msecs" Loading src/scratch_clojure/core.clj... done "Elapsed time: 223.200787 msecs" Loading src/scratch_clojure/core.clj... done "Elapsed time: 287.63579 msecs" Loading src/scratch_clojure/core.clj... done "Elapsed time: 244.927825 msecs" Loading src/scratch_clojure/core.clj... done "Elapsed time: 274.146199 msecs" 的代码(因为我重写了它)。跳出来的第一件事是is-relatively-prime?函数。 count序列是一个序列,而不是一个向量,这意味着primes函数还必须遍历完整列表以添加其中有多少元素。更糟糕的是,如果我们从一个例如10​​个候选者的列表开始,它在第一次遍历循环时全部走10,然后在下一个循环中走剩余的9个候选者,然后剩下8个,依此类推。随着素数的增加,我们将在计数函数上花费越来越多的时间,所以这可能是我们的瓶颈。

我们希望摆脱count,这表明我们可以使用count进行循环的更惯用的方式。像这样:

if-let

如果候选列表为空,(defn is-relatively-prime? [n candidates] (if-let [current (first candidates)] (if (zero? (rem n current)) false (recur n (rest candidates))) true)) 函数将返回(first candidates),如果发生这种情况,nil函数会注意到,并自动跳转到else子句,其中包含case是我们的返回结果“true”。否则,我们将执行“then”子句,并可以测试if-let是否可被当前候选者整除。如果是,我们返回false,否则我们会与其他候选人一起重复。我也利用n函数,因为我可以。让我们看看这会给我们带来什么。

zero?

非常戏剧化,嗯?我是一名中级Clojure编码器,对内部结构有着非常粗略的理解,所以我的分析用了一些盐,但基于这些数字,我猜你会被Loading src/scratch_clojure/core.clj... done "Elapsed time: 9.981985 msecs" Loading src/scratch_clojure/core.clj... done "Elapsed time: 8.011646 msecs" Loading src/scratch_clojure/core.clj... done "Elapsed time: 8.154197 msecs" Loading src/scratch_clojure/core.clj... done "Elapsed time: 9.905292 msecs" Loading src/scratch_clojure/core.clj... done "Elapsed time: 8.215208 msecs" 咬伤。

“快速”代码正在使用的另一个优化是你的,而count测试每当is-relatively-prime?平方大于current时就会豁免如果你能把它放进去,你可能会加速你的代码。但我认为n是你正在寻找的主要内容。

答案 1 :(得分:0)

我将继续根据@ manutter的解决方案加快速度。

(declare primes)

(defn is-relatively-prime? [n candidates]
  (if-let [current (first candidates)]
    (if (zero? (rem n current))
      false
      (recur n (rest candidates)))
    true))

(defn get-next-prime [largest-prime-so-far]
  (let [primes-so-far (concat (take-while #(not= largest-prime-so-far %) primes) [largest-prime-so-far])]
    (loop [n (+ (last primes-so-far) 2)]
      (if
        (is-relatively-prime? n (rest primes-so-far))
        n
        (recur (+ n 2))))))

(def primes
  (lazy-cat '(2 3) (map get-next-prime (rest primes))))

(time (first (drop 10000 primes)))

“经历时间:14092.414513 msecs”

确定。首先,让我们添加current^2 > n优化:

(defn get-next-prime [largest-prime-so-far]
  (let [primes-so-far (concat (take-while #(not= largest-prime-so-far %) primes) [largest-prime-so-far])]
    (loop [n (+ (last primes-so-far) 2)]
      (if
          (is-relatively-prime? n
                                (take-while #(<= (* % %) n)
                                            (rest primes-so-far)))
        n
        (recur (+ n 2))))))

user> (time (first (drop 10000 primes)))
"Elapsed time: 10564.470626 msecs"
104743

尼斯。现在让我们仔细看看get-next-prime

如果仔细检查算法,您会注意到: (concat (take-while #(not= largest-prime-so-far %) primes) [largest-prime-so-far])实际上等于我们到目前为止找到的所有素数,(last primes-so-far)实际上是largest-prime-so-far。所以让我们重写一下:

(defn get-next-prime [largest-prime-so-far]
  (loop [n (+ largest-prime-so-far 2)]
    (if (is-relatively-prime? n
          (take-while #(<= (* % %) n) (rest primes)))
      n
      (recur (+ n 2)))))

user> (time (first (drop 10000 primes)))
"Elapsed time: 142.676634 msecs"
104743

让我们再增加一个数量级:

user> (time (first (drop 100000 primes)))
"Elapsed time: 2615.910723 msecs"
1299721

哇!这真是令人兴奋!

但并非全部。我们来看看is-relatively-prime函数: 它只是检查没有候选人平均分配数字。所以它实际上是not-any?库函数的作用。所以我们只需在get-next-prime中替换它。

(declare primes)

(defn get-next-prime [largest-prime-so-far]
  (loop [n (+ largest-prime-so-far 2)]
    (if (not-any? #(zero? (rem n %))
                  (take-while #(<= (* % %) n)
                              (rest primes)))
      n
      (recur (+ n 2)))))

(def primes
  (lazy-cat '(2 3) (map get-next-prime (rest primes))))

效率更高

user> (time (first (drop 100000 primes)))
"Elapsed time: 2493.291323 msecs"
1299721

显然更清洁,更短。