Уникнення витоків пам’яті за допомогою перелічувачів 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 чи в моєму коді. Інтуїтивно, я очікую, що цей код вимагатиме лише (на порядок) 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 .
Аарон Новструп

1
Це не буде весело, але ви можете спробувати -XX:+HeapDumpOnOutOfMemoryErrorі проаналізувати дамп з eclipse MAT eclipse.org/mat, щоб побачити, який рядок коду тримається для масивів.
huynhjl

10
@huynhjl FWIW, я спробував проаналізувати купу як JProfiler, так і MAT, але не зміг пройти всі посилання на класи анонімних функцій і т. д. Scala справді потрібні спеціальні інструменти для подібних речей.
Аарон Новструп

Що робити, якщо немає витоку, і це просто те, що ви робите, вимагає динамічно зростаючої кількості пам'яті? Ви можете легко копіювати zipWithIndex без конкретної конструкції FP, просто підтримуючи varлічильник під час руху .
Єзекіїль Віктор

@EzekielVictor Я не впевнений, що я розумію коментар. Ви припускаєте, що додавання єдиного Longіндексу за шматок змінить алгоритм з постійного на незмінний простір купи? Версія, яка не містить блискавки, явно використовує постійний простір у купі, оскільки вона може "обробити" стільки фрагментів, скільки ви готові чекати.
Аарон Новструп

Відповіді:


4

Це стане малою втіхою для тих, хто застряг із старішим iterateeAPI, але я нещодавно переконався, що еквівалентний тест проходить проти API-файлу scalaz-stream . Це новіший 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 пам'яті, виділеної за часом).

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.