Tránh rò rỉ bộ nhớ với Scalaz 7 zipWithIndex / liệt kê nhóm


106

Lý lịch

Như đã lưu ý trong câu hỏi này , tôi đang sử dụng các lần lặp Scalaz 7 để xử lý một luồng dữ liệu lớn (tức là không bị ràng buộc) trong không gian đống không đổi.

Mã của tôi trông như thế này:

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

Vấn đề

Có vẻ như tôi đã gặp phải tình trạng rò rỉ bộ nhớ, nhưng tôi không đủ quen thuộc với Scalaz / FP để biết liệu lỗi nằm trong Scalaz hay trong mã của tôi. Theo trực giác, tôi mong đợi mã này chỉ yêu cầu (theo thứ tự) P nhân với Chunkkhông gian -size.

Lưu ý: Tôi đã tìm thấy một câu hỏi tương tự mà trong đó câu hỏi này OutOfMemoryErrorđã gặp phải, nhưng mã của tôi không sử dụng consume.

Thử nghiệm

Tôi đã chạy một số thử nghiệm để thử và cô lập vấn đề. Tóm lại, rò rỉ chỉ xuất hiện khi cả hai zipWithIndexgroupđược sử dụng.

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

Mã cho các bài kiểm tra:

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
}

Câu hỏi

  • Có phải lỗi trong mã của tôi không?
  • Làm thế nào tôi có thể làm cho điều này hoạt động trong không gian đống không đổi?

6
Tôi đã kết thúc báo cáo đây là một vấn đề trong Scalaz .
Aaron Novstrup

1
Sẽ không vui chút nào, nhưng bạn có thể thử -XX:+HeapDumpOnOutOfMemoryErrorvà phân tích kết xuất với eclipse MAT eclipse.org/mat để xem dòng mã nào đang giữ các mảng.
huynhjl

10
@huynhjl FWIW, tôi đã thử phân tích đống với cả JProfiler và MAT nhưng hoàn toàn không thể lướt qua tất cả các tham chiếu đến các lớp hàm ẩn danh, v.v. Scala thực sự cần các công cụ chuyên dụng cho loại việc này.
Aaron Novstrup

Điều gì sẽ xảy ra nếu không có rò rỉ và chỉ là những gì bạn đang làm đòi hỏi một lượng bộ nhớ tăng lên đáng kể? Bạn có thể dễ dàng sao chép zipWithIndex mà không cần cấu trúc FP cụ thể đó bằng cách duy trì một bộ varđếm khi bạn thực hiện.
Ezekiel Victor

@EzekielVictor Tôi không chắc mình hiểu nhận xét. Bạn đang gợi ý rằng việc thêm một Longchỉ mục duy nhất cho mỗi đoạn sẽ thay đổi thuật toán từ không gian heap không đổi thành không đổi? Phiên bản không nén rõ ràng sử dụng không gian đống không đổi, vì nó có thể "xử lý" bao nhiêu phần mà bạn sẵn sàng chờ đợi.
Aaron Novstrup

Câu trả lời:


4

Điều này sẽ có chút an ủi cho bất kỳ ai bị mắc kẹt với iterateeAPI cũ hơn , nhưng gần đây tôi đã xác minh rằng một bài kiểm tra tương đương sẽ vượt qua API scalaz-stream . Đây là một API xử lý luồng mới hơn nhằm thay thế iteratee.

Để hoàn thiện, đây là mã kiểm tra:

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

Điều này sẽ hoạt động với bất kỳ giá trị nào cho ntham số (miễn là bạn sẵn sàng đợi đủ lâu) - Tôi đã thử nghiệm với 2 ^ 14 mảng 32MiB (tức là, tổng cộng một nửa TiB bộ nhớ được phân bổ theo thời gian).

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.