在Clojure中处理大文件时出现了内存不足错误。

5

我正在参加algo-class.org课程,其中一个编程作业提供了以下格式的文件:

1 2
1 5
2 535

有500万条这样的代码行,我想读取文件并将其转换为整数向量的向量,例如:[[1 2][1 5][2 535]...]

(defn to-int-vector [s]
    (vec (map #(Integer/parseInt %) (re-seq #"\w+" s))))    

(def ints (with-open [rdr (clojure.java.io/reader "<file>")]
               (doall (map to-int-vector (line-seq rdr)))))

我认为这样做的好处是不会将整个文件存储在内存中,而只生成一个大的整数向量。但是,我从中得到了OutOfMemoryError错误。我尝试通过运行rand-int来生成相同大小和格式的向量,结果正常。

看起来,内存问题是由生成的临时对象引起的?在Clojure中处理这种情况的理想方法是什么?

更新:

是的,我意识到我正在持有整个整数向量。我增加了堆大小,现在它可以工作了。我很惊讶一个包含500万个元素(1000万个整数)的向量会占用这么多内存——我必须为JVM分配3G。是否有其他方法可以降低内存使用率?

3个回答

5
你无法想象实现的lazy序列会带来多少开销。我在64位操作系统上进行了测试:大约是120字节。这是每个lazy序列成员的纯开销。另一方面,向量的开销相当低,如果向量足够大,则基本与Java数组相同。因此,请尝试用vec替换doall
让我们看看没有开销时您花费了多少内存。您有500万对整数--也就是5e6 x 8 = 40 MB。您可以使用short来节省50%的空间(请注意---这不包括父集合的开销,每个保存一对的向量实例都有自己的开销)。
节省的下一步是同时为外部集合和对使用一个原始数组。由于数组是可序列化的,并且与语言非常好地集成,因此它仍然可以是一个非常实用的解决方案。要做到这一点,您只需将两个vec的出现替换为to-array
更新:
由于IntegerShort都是完全成熟的对象,因此它们之间的差异并不是很大。将数字对存储为原始数组(使用short-arrayint-array而不是to-array)将节省更多空间。

是的,vec和to-array有助于减少内存。谢谢! - awh
更新了我的回答 - 尝试使用 int-arrayshort-array 替代 to-array 来处理数字对。 - Marko Topolnik

2

同时利用惰性和封装with-open很困难。在您的情况下,惰性非常重要,因为它使得只有行或int-vector序列的“相关”部分在内存中。

解决该问题的一种方法是不封装with-open,并将整个行处理逻辑包含在with-open表单的动态范围内:

(with-open [rdr (clojure.java.io/reader "<file>")]
  (doseq [int-vector (map to-int-vector (line-seq rdr))]
    (process int-vector)))

这里有两个重要细节,一是不将行和int-vector序列存储起来,二是仅在 with-open 表单中使用它们。这些细节确保已处理的序列部分可以被垃圾回收,并且文件流在整个处理过程中保持打开状态。


1

(def ints) 中的 def 会确保整个结果存储在内存中,因为每行数字都存储在一个集合中(在这种情况下是 vec),而集合也占用空间,因此内存大小至少与文件一样大。

此外,默认情况下 Java 将拒绝使用计算机中的所有内存,您可能需要设置 maxHeapSize 参数。

如果您从新的 repl 开始(不包含任何大型列表),您是否仍然会耗尽内存?


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接