【问题标题】:Clojure Lazy Seq Results in Stack OverflowClojure Lazy Seq 导致堆栈溢出
【发布时间】:2015-12-21 07:57:30
【问题描述】:

我想计算一个惰性素数序列。

界面如下:

user=> (take 10 primes)
(2 3 5 7 11 13 17 19 23 29)

到目前为止,一切都很好。

但是,当我取 500 个素数时,这会导致堆栈溢出。

                  core.clj:  133  clojure.core/seq
                  core.clj: 2595  clojure.core/filter/fn
              LazySeq.java:   40  clojure.lang.LazySeq/sval
              LazySeq.java:   49  clojure.lang.LazySeq/seq
                   RT.java:  484  clojure.lang.RT/seq
                  core.clj:  133  clojure.core/seq
                  core.clj: 2626  clojure.core/take/fn
              LazySeq.java:   40  clojure.lang.LazySeq/sval
              LazySeq.java:   49  clojure.lang.LazySeq/seq
                 Cons.java:   39  clojure.lang.Cons/next
              LazySeq.java:   81  clojure.lang.LazySeq/next
                   RT.java:  598  clojure.lang.RT/next
                  core.clj:   64  clojure.core/next
                  core.clj: 2856  clojure.core/dorun
                  core.clj: 2871  clojure.core/doall
                  core.clj: 2910  clojure.core/partition/fn
              LazySeq.java:   40  clojure.lang.LazySeq/sval
              LazySeq.java:   49  clojure.lang.LazySeq/seq
                   RT.java:  484  clojure.lang.RT/seq
                  core.clj:  133  clojure.core/seq
                  core.clj: 2551  clojure.core/map/fn
              LazySeq.java:   40  clojure.lang.LazySeq/sval
              LazySeq.java:   49  clojure.lang.LazySeq/seq
                   RT.java:  484  clojure.lang.RT/seq
                  core.clj:  133  clojure.core/seq
                  core.clj: 3973  clojure.core/interleave/fn
              LazySeq.java:   40  clojure.lang.LazySeq/sval

我想知道这里有什么问题,更一般地说,在使用惰性序列时,我应该如何处理这类错误?

这里是代码。

(defn assoc-nth
  "Returns a lazy seq of coll, replacing every nth element by val

  Ex:
  user=> (assoc-nth [3 4 5 6 7 8 9 10] 2 nil)
  (3 nil 5 nil 7 nil 9 nil)
  "
  [coll n val]
  (apply concat
         (interleave
          (map #(take (dec n) %) (partition n coll)) (repeat [val]))))

(defn sieve
  "Returns a lazy seq of primes by Eratosthenes' method

  Ex:
  user=> (take 4 (sieve (iterate inc 2)))
  (2 3 5 7)

  user=> (take 10 (sieve (iterate inc 2)))
  (2 3 5 7 11 13 17 19 23 29)
  "
  [s]
  (lazy-seq
   (if (seq s)
     (cons (first s) (sieve
                      (drop-while nil? (assoc-nth (rest s) (first s) nil))))
     [])))

(def primes
  "Returns a lazy seq of primes

  Ex:
  user=> (take 10 primes)
  (2 3 5 7 11 13 17 19 23 29)
  "
  (concat [2] (sieve (filter odd? (iterate inc 3)))))

【问题讨论】:

  • 你的埃拉托色尼筛子的实现不是尾递归的(recur 函数从未使用过)所以这最终会破坏堆栈。 Clojure != Lisp,所以如果你从另一个 Lisp 版本中复制了一个实现,你需要重新设计它以使其在 Clojure 中尾递归(老实说,我看不出它在另一个 Lisp 中是如何尾递归的).. 乍一看,sieve 似乎需要重新完成以使其成为尾递归。祝你好运。
  • lazy-seq 可以通过 thunk 间接消除这个顾虑,所以这里还有一些其他的问题。
  • 我无法重现堆栈溢出,并且您的代码看起来不错。当我将限制从 500 增加到 1000 时,程序变得太慢以至于我无法耐心等待,而且堆使用量很大。
  • 每次构造下一个素数时,都使用惰性序列构建器(如 concat)在惰性序列上构建它。所以每次下一个素数都是从这个惰性调用构建的,在上面添加更多的惰性层。当你评估素数的第 n 个元素时,你有一大堆惰性序列调用另一个惰性序列等。

标签: clojure primes lazy-evaluation


【解决方案1】:

您正在生成大量占用堆栈空间的惰性序列。

分解:

user=> (iterate inc 3)
;; produces first lazy seq (don't run in repl!)
(3 4 5 6 7 8 9 10 11 12 13 ...)

user=> (filter odd? (iterate inc 3))
;; again, don't run in repl, but lazy seq #2
(3 5 7 9 11 13 ...)

我个人会通过(iterate #(+ 2 %) 3) 删除一个序列,但与整体问题相比,这只是沧海一粟。

现在我们开始为我们的输出创建一个新的惰性序列的筛子

(lazy-seq
 (cons 3 (sieve (drop-while nil? (assoc-nth '(5 7 9 ...) 3 nil)))))

进入 assoc-nth,我们开始创建更多惰性序列

user=> (partition 3 [5 7 9 11 13 15 17 19 21 23 25 ...])
;; lazy seq #4
((5 7 9) (11 13 15) (17 19 21) (23 25 27) ...)

user=> (map #(take 2 %) '((5 7 9) (11 13 15) (17 19 21) (23 25 27) ...))
;; lazy seq #5
((5 7) (11 13) (17 19) (23 25) ...)

user=> (interleave '((3 5) (9 11) (15 17) (21 23) ...) (repeat [nil]))
;; lazy seq #6 + #7 (repeat [nil]) is a lazy seq too
((5 7) [nil] (11 13) [nil] (17 19) [nil] (23 25) [nil] ...)

user=> (apply concat ...
;; lazy seq #8
(5 7 nil 11 13 nil 17 19 nil 23 25 nil ...)

所以你已经有 8 个惰性序列了,而我们只应用了第一个筛子。

回到筛子,它会做一个 drop-while,产生惰性序列号 9

user=> (drop-while nil? '(5 7 nil 11 13 nil 17 19 nil 23 25 nil ...))
;; lazy seq #9
(5 7 11 13 17 19 23 25 ...)

所以对于筛子的一次迭代,我们生成了 9 个序列。

在 sieve 的下一次迭代中,您生成了一个新的惰性序列(不是原来的!),并且该过程再次开始,每个循环生成另外 7 个序列。

下一个问题是assoc-nth 函数的效率。一系列数字和 nil 并不是标记特定因子的倍数的有效方法。你很快就会得到很多无法发布的非常长的序列,其中大部分都是空值,你需要阅读越来越长的序列来确定候选者是否是一个因素——这对于惰性序列读取 32 个条目的块以提高效率,因此当您分解 33 及以上时,您将拉入序列的多个块来处理。

此外,每次您以全新的顺序执行此操作时。

向您的 assoc-nth 添加一些调试,并在一个小样本上运行它会很快说明这一点。

user=> (sieve (take 50 (iterate #(+ 2 %) 3)))
assoc-nth n: 3 , coll: (5 7 9 11 13 15 17 19 21 23 25 27 29 31 33 35 37 39 41 43 45 47 49 51 53 55 57 59 61 63 65 67 69 71 73 75 77 79 81 83 85 87 89 91 93 95 97 99 101)
returning r: (5 7 nil 11 13 nil 17 19 nil 23 25 nil 29 31 nil 35 37 nil 41 43 nil 47 49 nil 53 55 nil 59 61 nil 65 67 nil 71 73 nil 77 79 nil 83 85 nil 89 91 nil 95 97 nil)
assoc-nth n: 5 , coll: (7 nil 11 13 nil 17 19 nil 23 25 nil 29 31 nil 35 37 nil 41 43 nil 47 49 nil 53 55 nil 59 61 nil 65 67 nil 71 73 nil 77 79 nil 83 85 nil 89 91 nil 95 97 nil)
returning r: (7 nil 11 13 nil 17 19 nil 23 nil nil 29 31 nil nil 37 nil 41 43 nil 47 49 nil 53 nil nil 59 61 nil nil 67 nil 71 73 nil 77 79 nil 83 nil nil 89 91 nil nil)
assoc-nth n: 7 , coll: (nil 11 13 nil 17 19 nil 23 nil nil 29 31 nil nil 37 nil 41 43 nil 47 49 nil 53 nil nil 59 61 nil nil 67 nil 71 73 nil 77 79 nil 83 nil nil 89 91 nil nil)
returning r: (nil 11 13 nil 17 19 nil 23 nil nil 29 31 nil nil 37 nil 41 43 nil 47 nil nil 53 nil nil 59 61 nil nil 67 nil 71 73 nil nil 79 nil 83 nil nil 89 nil)
;; ...
assoc-nth n: 17 , coll: (19 nil 23 nil nil 29 31 nil nil 37 nil 41 43 nil 47 nil nil 53 nil nil 59 61 nil nil)
returning r: (19 nil 23 nil nil 29 31 nil nil 37 nil 41 43 nil 47 nil nil)
assoc-nth n: 19 , coll: (nil 23 nil nil 29 31 nil nil 37 nil 41 43 nil 47 nil nil)
returning r: ()
(3 5 7 11 13 17 19)

这说明了如何需要越来越多的序列元素来生成素数列表,因为我从 3 到 99 的奇数开始,但只以 3 到 19 的素数结束,但在最后一次迭代 n=19 ,我的有限序列中没有足够的元素来消除更多的倍数。

有解决办法吗?

我认为您正在寻找“如何使惰性序列更好地完成我想做的事情?”的答案。在这种情况下,惰性序列将是一种权衡。您的算法有效,但会生成太多堆栈。您没有使用任何递归,因此您在序列上生成序列。首先要看的是如何使您的方法更具尾递归性,并丢失一些序列。我不能在这里提供解决方案,但我可以将其他解决方案链接到相同的问题,看看他们是否有比你做得更好的领域。

this implementation of Eratosthenes primes 有 2 个不错的(绑定数字)解决方案。一个是使用一个范围并对其进行筛选,另一个是使用布尔数组(快 40 倍)。带有它的associated article(在日语中,但谷歌在 Chrome 中翻译得很好)是一本很好的读物,它显示了幼稚方法与直接使用数组的非常集中的版本的时代,并键入提示以避免 jvm 中进一步的拆箱问题。

another SO question 有一个很好的实现,它使用瞬态值来提高效率。它的过滤方法与您的相似。

在每种情况下,都使用了不同的筛选方法来避免惰性序列,但是由于它们生成的素数有一个上限,它们可以用一般性来换取效率。

对于未绑定的情况,请查看this gist 以获得无限的素数序列。在我的测试中,它比之前的 SO 问题慢了 6 倍,但仍然相当不错。然而,实现方式却大不相同。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2023-03-14
    • 2015-05-21
    • 2014-02-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-09
    相关资源
    最近更新 更多