私は最近Scalaで遊び始め、次のことに出くわしました。以下は、ファイルの行を反復処理し、いくつかの処理を実行して、結果を別のファイルに書き込む4つの異なる方法です。これらの方法のいくつかは私が思うように機能し(そうするために多くのメモリを使用しますが)、いくつかは終わりなくメモリを消費します。
アイデアは、ScalaのgetLinesIteratorを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()
読み取っているファイルは、1MBの行で約10GBです。
最初の2つのオプションは、一定量のメモリ(〜100MB)を使用してファイルを反復処理します。これは私が期待することです。ここでの欠点は、イテレーターは1回しか使用できず、Scalaの名前による呼び出し規則を疑似反復可能として使用していることです。(参考までに、同等のc#コードは〜14MBを使用します)
3番目のメソッドは、TraverableOnceで定義されたtoIterableを呼び出します。これは機能しますが、同じ作業を行うために約2GBを使用します。Iterable全体をキャッシュできないため、メモリがどこに行くのかわかりません。
4つ目は、最も憂慮すべきことです。使用可能なすべてのメモリをすぐに使用し、OOM例外をスローします。さらに奇妙なのは、私がテストしたすべての操作(ドロップ、マップ、フィルター)に対してこれを実行することです。実装を見ると、それらのどれも多くの状態を維持していないようです(ドロップは少し疑わしいように見えますが、なぜそれは単にアイテムを数えないのですか?)。操作をしなくても問題なく動作します。
私の推測では、どこかで読み取られた各行への参照が維持されていると思いますが、その方法は想像できません。ScalaでIterablesを渡すときに同じメモリ使用量を見てきました。たとえば、ケース3(.toIterable)を取得し、それをIterable [String]をファイルに書き込むメソッドに渡すと、同じ爆発が発生します。
何か案は?