Evitar pérdidas de memoria con Scalaz 7 zipWithIndex / group enumeratees


106

Antecedentes

Como se señaló en esta pregunta , estoy usando iteraciones de Scalaz 7 para procesar un flujo de datos grande (es decir, sin límites) en un espacio de almacenamiento constante.

Mi código se ve así:

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

El problema

Parece que me he encontrado con una fuga de memoria, pero no estoy lo suficientemente familiarizado con Scalaz / FP para saber si el error está en Scalaz o en mi código. Intuitivamente, espero que este código requiera solo (en el orden de) P veces el Chunkespacio de tamaño.

Nota: Encontré una pregunta similar en la que OutOfMemoryErrorse encontró un, pero mi código no está usando consume.

Pruebas

Ejecuté algunas pruebas para intentar aislar el problema. En resumen, la fuga solo parece surgir cuando se utilizan ambos zipWithIndexy 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

Código para las pruebas:

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
}

Preguntas

  • ¿Está el error en mi código?
  • ¿Cómo puedo hacer que esto funcione en un espacio de pila constante?

6
Terminé informando esto como un problema en Scalaz .
Aaron Novstrup

1
No será divertido, pero podría intentar -XX:+HeapDumpOnOutOfMemoryErroranalizar el volcado con eclipse MAT eclipse.org/mat para ver qué línea de código se aferra a las matrices.
huynhjl

10
@huynhjl FWIW, intenté analizar el montón con JProfiler y MAT, pero no pude recorrer todas las referencias a clases de funciones anónimas, etc. Scala realmente necesita herramientas dedicadas para este tipo de cosas.
Aaron Novstrup

¿Qué pasa si no hay fugas y es solo que lo que está haciendo requiere una cantidad de memoria enormemente creciente? Puede replicar fácilmente zipWithIndex sin esa construcción FP en particular simplemente manteniendo un varcontador sobre la marcha .
Ezekiel Victor

@EzekielVictor No estoy seguro de entender el comentario. ¿Está sugiriendo que agregar un solo Longíndice por fragmento cambiaría el algoritmo de espacio de pila constante a no constante? La versión sin cremallera utiliza claramente un espacio de pila constante, porque puede "procesar" tantos fragmentos como esté dispuesto a esperar.
Aaron Novstrup

Respuestas:


4

Esto será un pequeño consuelo para cualquiera que esté atascado con la iterateeAPI anterior , pero recientemente verifiqué que pasa una prueba equivalente contra la API scalaz-stream . Esta es una API de procesamiento de transmisión más nueva que está destinada a reemplazar iteratee.

Para completar, aquí está el código de prueba:

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

Esto debería funcionar con cualquier valor para el nparámetro (siempre que esté dispuesto a esperar lo suficiente). Probé con 2 ^ 14 matrices de 32MiB (es decir, un total de medio TiB de memoria asignada a lo largo del tiempo).

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.