6

我最近开始使用 Scala 并遇到以下问题。下面是 4 种不同的方法来遍历文件的行,做一些事情,并将结果写入另一个文件。其中一些方法正如我所想的那样工作(尽管使用大量内存来这样做),而有些则无休止地消耗内存。

这个想法是将 Scala 的 getLines Iterator 包装为一个 Iterable。我不在乎它是否多次读取文件——这就是我期望它做的事情。

这是我的复制代码:

class FileIterable(file: java.io.File) extends Iterable[String] {
  override def iterator = io.Source.fromFile(file).getLines
}

// Iterator

// Option 1: Direct iterator - holds at 100MB
def lines = io.Source.fromFile(file).getLines

// Option 2: Get iterator via method - holds at 100MB
def lines = new FileIterable(file).iterator

// Iterable

// Option 3: TraversableOnce wrapper - holds at 2GB
def lines = io.Source.fromFile(file).getLines.toIterable

// Option 4: Iterable wrapper - leaks like a sieve
def lines = new FileIterable(file)

def values = lines
      .drop(1)
      //.map(l => l.split("\t")).map(l => l.reduceLeft(_ + "|" + _))
      //.filter(l => l.startsWith("*"))

val writer = new java.io.PrintWriter(new File("out.tsv"))
values.foreach(v => writer.println(v))
writer.close()

它正在读取的文件约为 10GB,行数为 1MB。

前两个选项使用恒定的内存量(~100MB)迭代文件。这是我所期望的。这里的缺点是迭代器只能使用一次,并且它使用 Scala 的按名称调用约定作为伪迭代。(作为参考,等效的 c# 代码使用 ~14MB)

第三个方法调用定义在 TraverableOnce 中的 toIterable。这个工作,但它使用大约 2GB 来做同样的工作。不知道内存的去向,因为它无法缓存整个 Iterable。

第四个是最令人震惊的——它立即使用所有可用内存并抛出 OOM 异常。更奇怪的是,它对我测试过的所有操作都执行此操作:drop、map 和 filter。查看实现,它们似乎都没有保持太多状态(尽管下降看起来有点可疑 - 为什么它不只计算项目?)。如果我不做任何操作,它工作正常。

我的猜测是,它在某处维护对所读取的每一行的引用,尽管我无法想象如何。在 Scala 中传递 Iterables 时,我看到了相同的内存使用情况。例如,如果我采用案例 3 (.toIterable) 并将其传递给将 Iterable[String] 写入文件的方法,我会看到同样的爆炸。

有任何想法吗?

4

1 回答 1

6

请注意ScalaDoc 的Iterable说法:

此 trait 的实现需要提供一个带有签名的具体方法:

  def iterator: Iterator[A]

他们还需要提供一种newBuilder为同类集合创建构建器的方法。

由于您没有为 提供实现newBuilder,因此您将获得默认实现,它使用 aListBuffer并因此尝试将所有内容放入内存中。

您可能希望实现Iterable.drop

def drop(n: Int) = iterator.drop(n).toIterable

但这会破坏集合库的表示不变性(即iterator.toIterable返回 a Stream,而您想List.drop返回 aList等 - 因此需要这个Builder概念)。

于 2012-09-20T06:01:51.393 回答