math.combinatorics中的Clojure延迟序列导致OutOfMemory(OOM)错误

时间:2013-04-24 14:36:37

标签: clojure lazy-evaluation

math.combinatorics的文档指出所有函数都返回延迟序列。

但是,如果我尝试使用大量数据运行subsets

(last (combinatorics/subsets (range 20)))
;OutOfMemoryError Java heap space  clojure.lang.RT.cons (RT.java:559)

我收到OutOfMemory错误。

运行

(last (range))

烧毁CPU,但不会返回错误。

Clojure似乎并不像解释in this Stack Overflow question那样“保持头脑”。

为什么会发生这种情况以及如何在子集中使用更大的范围?

更新

正如评论所暗示的那样,它似乎适用于某些人的计算机。所以我将发布我的系统配置

我运行Mac(10.8.3)并使用Clojure安装Homebrew(1.5.1)。

我的Java版本是:

% java -version
java version "1.6.0_45"
Java(TM) SE Runtime Environment (build 1.6.0_45-b06-451-11M4406)
Java HotSpot(TM) 64-Bit Server VM (build 20.45-b01-451, mixed mode)

我没有更改任何默认设置。我还通过删除~/.m2文件夹重新安装了所有依赖项。

我的projects.clj

我使用的命令就是这个

% lein repl
nREPL server started on port 61774
REPL-y 0.1.10
Clojure 1.5.1
=> (require 'clojure.math.combinatorics)
nil
=> (last (clojure.math.combinatorics/subsets (range 20)))
OutOfMemoryError Java heap space  clojure.lang.RT.cons (RT.java:570)
or
OutOfMemoryError Java heap space  clojure.math.combinatorics/index-combinations/fn--1148/step--1164 (combinatorics.clj:64)

我在同事的笔记本电脑上测试了这个问题,他遇到了同样的问题,但他也在Mac上。

5 个答案:

答案 0 :(得分:3)

问题在于subsets使用mapcatmapcat不够懒,因为它使用了apply,它实现并保存了一些要连接的元素。见a very nice explanation here。在子集中使用该链接的lazier mapcat版本应解决问题:

(defn my-mapcat
   [f coll]
   (lazy-seq
     (if (not-empty coll)
      (concat
      (f (first coll))
     (my-mapcat f (rest coll))))))

(defn subsets
  "All the subsets of items"
  [items]
  (my-mapcat (fn [n] (clojure.math.combinatorics/combinations items n))
  (range (inc (count items)))))

 (last (subsets (range 50))) ;; this will take hours to compute, good luck with it!

答案 1 :(得分:3)

您想要计算具有1000个元素的集合的幂集吗?你知道那将有2 ^ 1000个元素,对吧?这是如此之大,我甚至找不到一个很好的方式来描述它是多么巨大。如果您正在尝试使用这样的一组,并且您可以懒惰地这样做,那么您的问题将不是内存:它将是计算时间。假设您拥有一台具有无限内存的超级计算机,每纳秒可以处理万亿项目:每秒处理10 ^ 21项,或每年约10 ^ 29项。即使这台超级计算机也需要比宇宙的生命周期更长的时间来处理(subsets (range 1000))的项目。

所以我要说,不要再担心这个集合的内存使用情况了,并且研究一种算法,该算法不涉及遍历具有比宇宙中的原子更多元素的序列。

答案 2 :(得分:2)

问题既不是apply,也不是concat,也不是mapcat

dAni's answer,他重新实现mapcat,意外导致解决问题,但其背后的推理并不正确。此外,他的回答指向一篇文章,其中作者说“我相信问题在于应用”。 这显然是错误的,因为我将在下面解释。最后,手头的问题与this other one无关,apply导致非懒惰的评估确实

如果仔细观察,dAni和该文章的作者都会在不使用mapcat函数的情况下实现map。我将在下一个示例中显示该问题与map函数的实现方式有关。

要证明该问题与applyconcat无关,请参阅mapcat的以下实施。它同时使用concatapply,但仍会实现完全懒惰:

(defn map
  ([f coll]
     (lazy-seq
      (when-let [s (seq coll)]
        (cons (f (first s)) (map f (rest s)))))))

(defn mapcat [f & colls]
  (apply concat (apply map f colls)))

(defn range-announce! [x]
  (do (println "Returning sequence of" x)
      (range x)))

;; new fully lazy implementation prints only 5 lines
(nth (mapcat range-announce! (range)) 5)

;; clojure.core version still prints 32 lines
(nth (clojure.core/mapcat range-announce! (range)) 5)

通过重新实现map函数,可以实现上述代码中的完全惰性。实际上,mapcatclojure.core一样exactly the same way已实现,但它完全是懒惰的。上面的map实现在示例中有点简化,因为它只支持单个参数,但即使用整个可变参数签名实现它也会有相同的效果:完全懒惰 。因此,我们发现此处的问题既不是apply也不是concat。此外,我们发现真正的问题必须与mapclojure.core函数的实现方式有关。我们来看看它:

(defn map
  ([f coll]
   (lazy-seq
    (when-let [s (seq coll)]
      (if (chunked-seq? s)
        (let [c (chunk-first s)
              size (int (count c))
              b (chunk-buffer size)]
          (dotimes [i size]
              (chunk-append b (f (.nth c i))))
          (chunk-cons (chunk b) (map f (chunk-rest s))))
        (cons (f (first s)) (map f (rest s))))))))

可以看出clojure.core实现与之前的“简化”版本完全相同,除了true表达式的if (chunked-seq? s)分支。基本上clojure.core/map有一个处理 chunked sequences 的输入序列的特殊情况。

分块序列通过评估32个块而不是一次严格一个来破坏懒惰。在评估深层嵌套的分块序列时,这变得非常明显,就像subsets的情况一样。在Clojure 1.1中引入了分块序列,并且升级了许多核心功能以识别和处理它们,包括map。引入它们的主要目的是提高某些流处理场景的性能,但可以说它们使得更难以推断程序的懒惰特征。您可以阅读分块序列herehere。另请查看此问题here

真正的问题是range返回一个chunked seq,并由subsets在内部使用。 David James修补程序subsets建议 unchunk 内部由range创建的序列。

答案 3 :(得分:1)

此问题已在项目的故障单跟踪器中提出:Clojure JIRA: OutOfMemoryError with combinatorics/subsets。在那里,你可以找到Andy Fingerhut的补丁。它对我有用。请注意,该修补程序与mapcat variation suggested by another answer不同。

答案 4 :(得分:0)

在没有命令行参数的情况下,JVM的启动堆大小参数由各种人体工程学确定

默认值(JDK 6)

   
   initial heap size    memory / 64
   maximum heap size    MIN(memory / 4, 1GB)

但您可以使用-Xmx-Xms args强制使用绝对值 您可以找到更多详细信息here