Scalaz 7のzipWithIndex / group列挙子によるメモリリークの回避


106

バックグラウンド

この質問で述べたように、私はScalaz 7反復を使用して、一定のヒープスペースでデータの大規模な(つまり、制限のない)ストリームを処理しています。

私のコードは次のようになります:

type ErrorOrT[M[+_], A] = EitherT[M, Throwable, A]
type ErrorOr[A] = ErrorOrT[IO, A]

def processChunk(c: Chunk, idx: Long): Result

def process(data: EnumeratorT[Chunk, ErrorOr]): IterateeT[Vector[(Chunk, Long)], ErrorOr, Vector[Result]] =
  Iteratee.fold[Vector[(Chunk, Long)], ErrorOr, Vector[Result]](Nil) { (rs, vs) =>
    rs ++ vs map { 
      case (c, i) => processChunk(c, i) 
    }
  } &= (data.zipWithIndex mapE Iteratee.group(P))

問題

メモリリークが発生したようですが、ScalazまたはFPに精通していないため、バグがScalazにあるのか、コードにあるのかがわかりません。直感的に、このコードでは-sizeスペースのP倍(Chunk約)しか必要としないと思います。

注:に似た質問が見つかりましOutOfMemoryErrorが、私のコードではを使用していませんconsume

テスト中

問題を特定するためにいくつかのテストを実行しました。要約すると、リークは、zipWithIndexとの両方をgroup使用した場合にのみ発生するように見えます。

// no zipping/grouping
scala> (i1 &= enumArrs(1 << 25, 128)).run.unsafePerformIO
res47: Long = 4294967296

// grouping only
scala> (i2 &= (enumArrs(1 << 25, 128) mapE Iteratee.group(4))).run.unsafePerformIO
res49: Long = 4294967296

// zipping and grouping
scala> (i3 &= (enumArrs(1 << 25, 128).zipWithIndex mapE Iteratee.group(4))).run.unsafePerformIO
java.lang.OutOfMemoryError: Java heap space

// zipping only
scala> (i4 &= (enumArrs(1 << 25, 128).zipWithIndex)).run.unsafePerformIO
res51: Long = 4294967296

// no zipping/grouping, larger arrays
scala> (i1 &= enumArrs(1 << 27, 128)).run.unsafePerformIO
res53: Long = 17179869184

// zipping only, larger arrays
scala> (i4 &= (enumArrs(1 << 27, 128).zipWithIndex)).run.unsafePerformIO
res54: Long = 17179869184

テストのコード:

import scalaz.iteratee._, scalaz.effect.IO, scalaz.std.vector._

// define an enumerator that produces a stream of new, zero-filled arrays
def enumArrs(sz: Int, n: Int) = 
  Iteratee.enumIterator[Array[Int], IO](
    Iterator.continually(Array.fill(sz)(0)).take(n))

// define an iteratee that consumes a stream of arrays 
// and computes its length
val i1 = Iteratee.fold[Array[Int], IO, Long](0) { 
  (c, a) => c + a.length 
}

// define an iteratee that consumes a grouped stream of arrays 
// and computes its length
val i2 = Iteratee.fold[Vector[Array[Int]], IO, Long](0) { 
  (c, as) => c + as.map(_.length).sum 
}

// define an iteratee that consumes a grouped/zipped stream of arrays
// and computes its length
val i3 = Iteratee.fold[Vector[(Array[Int], Long)], IO, Long](0) {
  (c, vs) => c + vs.map(_._1.length).sum
}

// define an iteratee that consumes a zipped stream of arrays
// and computes its length
val i4 = Iteratee.fold[(Array[Int], Long), IO, Long](0) {
  (c, v) => c + v._1.length
}

ご質問

  • 私のコードにバグはありますか?
  • 一定のヒープ領域でこれをどのように機能させることができますか?

6
私はこれをScalazの問題として報告してしまいました。
Aaron Novstrup 2013年

1
面白くはありませんが-XX:+HeapDumpOnOutOfMemoryError、eclipse MAT eclipse.org/matでダンプを分析して、配列に保持されているコード行を確認できます。
huynhjl 2013年

10
@huynhjl FWIW、JProfilerとMATの両方でヒープを分析してみましたが、無名関数クラスなどへのすべての参照を完全に処理することはできませんでした。Scalaには、この種の専用のツールが本当に必要です。
アーロンノヴストラップ2013年

リークがなく、あなたがやっていることが大幅に増加するメモリ量を必要としているだけの場合はどうなりますか?varカウンターを維持するだけで、特定のFP構成なしでzipWithIndexを簡単に複製できます。
エゼキエルビクター

@EzekielVictorコメントが理解できません。Longチャンクごとに単一のインデックスを追加すると、アルゴリズムが定数ヒープスペースから非定数ヒープスペースに変更されることを示唆していますか?非圧縮バージョンは、一定のヒープスペースを使用します。待機するのと同じ数のチャンクを「処理」できるためです。
Aaron Novstrup、2014年

回答:


4

これは、古いiterateeAPIを使い続けている人にとってはほとんど慰めにはなりませんが、最近、同等のテストがscalaz-stream APIに対してパスすることを確認しました。これは、を置き換えることを目的とした新しいストリーム処理APIですiteratee

完全を期すために、テストコードを次に示します。

// create a stream containing `n` arrays with `sz` Ints in each one
def streamArrs(sz: Int, n: Int): Process[Task, Array[Int]] =
  (Process emit Array.fill(sz)(0)).repeat take n

(streamArrs(1 << 25, 1 << 14).zipWithIndex 
      pipe process1.chunk(4) 
      pipe process1.fold(0L) {
    (c, vs) => c + vs.map(_._1.length.toLong).sum
  }).runLast.run

これは、nパラメーターの任意の値で機能するはずです(十分に長く待つことをいとわない場合)-2 ^ 14個の32MiB配列(つまり、時間の経過に応じて割り当てられたメモリの合計TiBの半分)でテストしました。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.