【问题标题】:Clojure lazy sequences in math.combinatorics results in OutOfMemory (OOM) Errormath.combinatorics 中的 Clojure 惰性序列导致 OutOfMemory (OOM) 错误
【发布时间】:2013-04-18 04:08:17
【问题描述】:

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 解释的那样“抓住头”。

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

更新

正如 cmets 建议的那样,它似乎可以在某些人的计算机上运行。所以我将发布我的系统配置

我运行 Mac (10.8.3) 并安装了 Clojure (1.5.1) 和 Homebrew

我的 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 上。

【问题讨论】:

  • 工作正常,没有 OOM 错误。您使用的是哪个 clojure 版本?
  • 适用于我在 Linux 上使用 Clojure 1.5.1 和默认 JVM 设置。
  • clojure 1.5.1 在 Mac 上(随自制软件安装)。我用更多细节更新了问题描述。
  • @Ankur user100464 运行时会发生什么(last (clojure.math.combinatorics/subsets (range 1000)))?你有什么 -Xmx 设置?

标签: clojure lazy-evaluation


【解决方案1】:

问题不在于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函数来实现的。事实上,mapcat 是在 exactly the same way 中实现的,就像在 clojure.core 中一样,但它完全是懒惰的。上面的map 实现为了示例而稍微简化了一点,因为它只支持单个参数,但即使使用整个可变参数签名来实现它也会起作用:完全懒惰。所以我们证明这里的问题既不是apply也不是concat。此外,我们表明真正的问题必须与map 函数在clojure.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的实现和我们之前的“简化”版本完全一样,除了if (chunked-seq? s)表达式的true分支。本质上,clojure.core/map 具有处理输入序列的特殊情况,这些输入序列是块序列

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

真正的问题是range 返回一个分块序列,并由subsets 在内部使用。 David James 推荐的修复将 subsets 修补为 unchunk range 在内部创建的序列。

【讨论】:

    【解决方案2】:

    该问题已在项目的票证跟踪器上提出:Clojure JIRA: OutOfMemoryError with combinatorics/subsets。在那里,您可以找到 Andy Fingerhut 的补丁。它对我有用。请注意,补丁不同于mapcat variation suggested by another answer

    【讨论】:

      【解决方案3】:

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

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

      【讨论】:

      • 你说得对。用子集计算一个大的解决方案集需要很长时间,并且在实际应用中不是一个可行的选择。但是,对于具有更实际用例的应用程序来说,拥有一个不易持续使用内存的子集版本也很重要。
      【解决方案4】:

      问题在于subsets 使用mapcat,而mapcat 不够懒,因为它使用apply 来实现并保存一些要连接的元素。见a very nice explanation here。在子集中使用该链接的惰性 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!
      

      【讨论】:

      • 我希望库具有恒定的内存使用率。我可以将 Xmx 增加到 4gb,但这几乎不能处理该范围内的 50 个元素。我想创建包含 1000 多个元素的集合子集。
      • 你用的是什么版本的java?
      • 我在 64 位 Mac OS X 上运行 java 版本“1.6.0_45”
      【解决方案5】:

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

      决定

      默认值(JDK 6)是

      初始堆大小内存 / 64 最大堆大小 MIN(内存 / 4, 1GB)

      但您可以使用 -Xmx-Xms 参数强制一个绝对值 你可以找到更多细节here

      【讨论】:

      • 设置 xx 不是问题。我试图理解为什么这个函数占用的内存比它应该的多。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-06-26
      • 2010-12-08
      • 2014-06-17
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多