Dividir los iteradores de Scala conduce a problemas de GCoverhead/JavaHeapSpace

Estoy procesando datos de gran tamaño con Scala, por lo que la memoria y el tiempo son un compañero aún más importante de lo que suele ser para mí. Estoy tratando de aumentar la velocidad de alguna evaluación subdividiendo el inicial Iterator[String] obtenido por getLines en un archivo fuente grande para hacer una subevaluación en paralelo y fusionar los resultados. Hago esto recursivamente slice- dividiendo el iterador en dos mitades y recuperando la función recursiva en cada subiterador. Ahora, me pregunto por qué obtengo la excepción GCoverhead o JavaHeapSpace, aunque los elementos "críticos" solo se evalúan una vez antes del paso de recursión (para obtener el tamaño del iterador), pero en mi opinión no en el paso de recursión, porque slice devuelve un iterador nuevamente (que no es estricto por implementación). El siguiente código (¡reducido!) fallará al aplicarse en un archivo de ~15 g antes de concatenar las sublistas.

yo suelo .duplicateen cada paso. Busqué la API, el documento de .duplicate dice "La implementación puede asignar almacenamiento temporal para los elementos iterados por un iterador pero aún no por el otro", pero aún no se ha iterado ningún elemento. ¿Podría alguien darme una pista de lo que está mal allí y cómo resolver este problema? ¡Muchas gracias!

type itType = Iterator[String]
def src = io.Source.fromFile(args(0)).getLines

// recursively divide into equal size blocks in divide&conquer fashion
def getSubItsDC(it: itType, depth: Int = 4) = {
    println("Getting length of file..")
    val totalSize = src.length
    println(totalSize)
    def rec(it_rec: itType = it, depth_rec: Int = depth, size: Int = totalSize): 
        List[itType] = depth_rec match {
            case n if n > 0 => 
                println(n)
                val (it1, it2) = it_rec.duplicate
                val newSize = size/2
                rec(it1 slice (0,newSize), n-1, newSize) ++ 
                    rec(it2 slice (newSize,size), n-1, newSize)
            case n if n == 0 => List(it_rec)
    }
    println("Starting recursion..")
    rec()
}
getSubItsDC(src)

En el REPL, el código se ejecuta igualmente rápido con un tamaño arbitrario de iteradores (cuando se codifica el tamaño total), por lo que asumí la pereza correcta.

preguntado el 12 de junio de 12 a las 16:06

1 Respuestas

Creo que es mejor que uses el itr grouped size para obtener una Iterator[Iterator[String]] (a GroupedIterator):

scala> val itr = (1 to 100000000).iterator grouped 1000000
itr: Iterator[Int]#GroupedIterator[Int] = non-empty iterator

Esto le permitirá fragmentar el procesamiento de partes de su archivo.

Por qué su solución usa demasiada memoria

Duplicar un Iterator is obviamente una operación que significa que el iterador puede tener que almacenar en caché sus valores calculados. Por ejemplo:

scala> val itr = (1 to 100000000).iterator
itr: Iterator[Int] = non-empty iterator

scala> itr filter (_ % 10000000 == 0) foreach println
10000000
....
100000000

Pero cuando tomo un duplicado:

scala> val (a, b) = (1 to 100000000).iterator.duplicate
a: Iterator[Int] = non-empty iterator
b: Iterator[Int] = non-empty iterator

scala> a filter (_ % 10000000 == 0) foreach println

//oh dear, garbage collecting
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded

En este ejemplo, mientras recorro a, para que b ser un duplicado, los elementos que a ha iterado sobre pero cuál b no tiene, necesita ser almacenado en caché

Respondido el 12 de junio de 12 a las 16:06

Gracias por tu respuesta, te lo agradezco! No estoy muy seguro de cómo su ejemplo de .duplicate explica mi problema de código, porque en realidad nunca itero sobre ningún elemento, ¿verdad? Necesito considerar el uso de .grouped. Tengo la sensación de que esto no es exactamente lo que estaba buscando, porque devuelve una especie de iterador de listas. Mi método recursivo, por el contrario, estaba destinado a devolver una Lista de iteradores (al revés) para que pueda llamar .par en esa lista sin ningún esfuerzo adicional. Agrupando primero y llamando .toList.par causará el mismo bloqueo de memoria. ¿Alguna idea para eso? - Wayne Jhukie

Bueno, tu solución no es recursiva de cola y usa Iterator.duplicate. No estoy seguro de por qué un iterador de Listas no es exactamente lo que está buscando. ¿Hay un truco mental Jedi aquí, tal vez? - oxbow_lakes

Estaba al tanto de la recursividad sin cola, pero ¿es este el problema? La profundidad de recursión es 4, por lo que generará una lista de máx. talla 16 (espero no equivocarme ahí). Uso duplicado, sí, pero no evalúo ninguno de los elementos, por lo que no se necesita almacenamiento en caché, ¿por qué sigue fallando? Su ejemplo también se ejecuta durante un tiempo, por lo que esto debe significar que realmente solo se almacenan en caché los elementos evaluados, ¿o me equivoco? No puedo usar Iterator[List], porque me gustaría enhebrar en List[Iterator] a través de par sin tener los sub-iteradores siendo evaluados antes. Vocación toList en Iterator[List] cargaría todos los datos en la memoria. - Wayne Jhukie

Argh está bien, podría intentarlo myGroupedIterator.next.par, déjame probar eso! Lo siento. - Wayne Jhukie

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.