Evitare perdite di memoria con Scalaz 7 zipWithIndex / group enumeratees


106

sfondo

Come notato in questa domanda , sto usando gli iterati di Scalaz 7 per elaborare un flusso di dati di grandi dimensioni (cioè illimitato) in uno spazio di heap costante.

Il mio codice ha questo aspetto:

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))

Il problema

Mi sembra di aver riscontrato una perdita di memoria, ma non ho abbastanza familiarità con Scalaz / FP per sapere se il bug è in Scalaz o nel mio codice. Intuitivamente, mi aspetto che questo codice richieda solo (nell'ordine di) P volte lo Chunkspazio -size.

Nota: ho trovato una domanda simile in cui è OutOfMemoryErrorstato riscontrato un messaggio di errore, ma il mio codice non è in uso consume.

analisi

Ho eseguito alcuni test per cercare di isolare il problema. Per riassumere, la perdita sembra verificarsi solo quando vengono utilizzati sia zipWithIndexe 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

Codice per le prove:

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
}

Domande

  • Il bug è nel mio codice?
  • Come posso fare in modo che funzioni in uno spazio heap costante?

6
Ho finito per segnalare questo come un problema in Scalaz .
Aaron Novstrup

1
Non sarà per niente divertente, ma potresti provare ad -XX:+HeapDumpOnOutOfMemoryErroranalizzare il dump con eclipse MAT eclipse.org/mat per vedere quale riga di codice sta trattenendo gli array.
huynhjl

10
@huynhjl FWIW, ho provato ad analizzare l'heap sia con JProfiler che con MAT, ma non sono riuscito a trovare tutti i riferimenti a classi di funzioni anonime, ecc. Scala ha davvero bisogno di strumenti dedicati per questo genere di cose.
Aaron Novstrup

Cosa succede se non ci sono perdite ed è solo che quello che stai facendo richiede una quantità di memoria enormemente crescente? Puoi facilmente replicare zipWithIndex senza quel particolare costrutto FP semplicemente mantenendo un varcontatore mentre procedi .
Ezekiel Victor

@ EzekielVictor Non sono sicuro di aver capito il commento. Stai suggerendo che l'aggiunta di un singolo Longindice per blocco cambierebbe l'algoritmo da spazio heap costante a non costante? La versione non zippata usa chiaramente uno spazio di heap costante, perché può "elaborare" tutti i blocchi che sei disposto ad aspettare.
Aaron Novstrup

Risposte:


4

Questo sarà una piccola consolazione per chiunque sia bloccato con l' iterateeAPI precedente , ma di recente ho verificato che un test equivalente viene superato con l' API scalaz-stream . Questa è una nuova API di elaborazione del flusso che intende sostituire iteratee.

Per completezza, ecco il codice del test:

// 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

Questo dovrebbe funzionare con qualsiasi valore per il nparametro (ammesso che tu sia disposto ad aspettare abbastanza a lungo) - Ho provato con 2 ^ 14 array da 32MiB (cioè, un totale di mezzo TiB di memoria allocata nel tempo).

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.