函数将Stream参数转发给另一个函数保留引用

时间:2018-09-06 16:57:53

标签: scala memory-management clojure garbage-collection sequence

我能够编写一个流处理功能drop(n,s),该功能可以扩展到非常大的流。但是,当我编写另一个函数nth(n,s)时,它接受了一个流s并将其转发到drop(n,s)时,似乎nth()正在“保持住”流。这样会导致OutOfMemoryError等于大n

代码如下:

import scala.annotation.tailrec
import scala.collection.immutable.Stream.cons

object Streams {

  def iterate[A](start: A, f: A => A): Stream[A] =
    cons(start, iterate(f(start), f))

  def inc(n:Int) = n + 1

  def naturals() =
    iterate(1, inc)

  @tailrec def drop[T](n : Int, s : Stream[T]) : Stream[T] =
    if (n <= 0 || s.isEmpty) s
    else drop(n-1, s.tail)

  // @inline didn't seem to help
  def nth[T](n : Int, s : Stream[T]) =
    drop(n,s).head

  def N = 1e7.toInt

  def main(args: Array[String]) {
    println(drop(N,naturals()).head) // works fine for large N
    println(nth(N, naturals())) // results in OutOfMemoryError for N set to 1e7.toInt and -Xmx10m
  }

}

我在以下Java问题上的经验:why does this Java method leak—and why does inlining it fix the leak?使我相信Scala为nth()生成的代码在调用{{1}之前清除s时不够积极。 }。 Clojure库技巧(Java技巧)(请参见链接的问题)在这里不起作用,因为所有Scala函数参数均为drop()(而非val),因此无法进行分配({{ 1}})。

如何用var来编写可扩展的null

以下是2009-2011年间与Scala编译器相关的错误(nth()是根据drop()实现的):https://issues.scala-lang.org/browse/SI-2239

我无法从该Scala错误票证中得知他们是如何解决的。故障单中有一个建议,建议解决此问题的唯一方法是在reduceLeft()中复制foldLeft()代码。我真的希望那不是答案。

更新:Andrey Tyukin的答案https://stackoverflow.com/a/52209383/156550对其进行了修复。现在我有:

foldLeft()

并且reduceLeft()可以很好地缩放。

1 个答案:

答案 0 :(得分:2)

这里是一种快速脏污的解决方案,只需要增加两个字符:

def nth[T](n : Int, s: => Stream[T]) = drop(n,s).head

在没有=>的情况下会发生以下情况:

  • 当流s作为参数传递给nth时,它是对naturals()先前创建的已经存在的值的引用。
  • 由于.headdrop(n, s)必须将流返回到nth的堆栈帧,因此nth的堆栈帧不能被丢弃,因此{ {1}}保留引用nth
  • 由于在s工作时,对参数s的引用由nth的堆栈帧保留,因此所有丢弃的前缀实际上无法被垃圾收集(这是因为{ {1}}保证,如果您按住它的头部并多次迭代,它将返回相同的结果。)

现在,如果您添加drop,则会发生以下情况:

  • 由于Stream=>的堆栈帧仍然无法丢弃
  • 但是nth不保留对传递给.head的流的开头的引用。它仅保留对产生nth thunk 的引用。
  • thunk本身不会占用大量内存,并且不包含对所创建流的头部的任何引用,因此可以对流的前缀进行垃圾收集。

附加说明(Dima的测试用例):

请注意,如果thunk本身仅返回对已经存在的drop的引用,则其行为仍然与没有Stream时的行为相同。例如,如果您的Stream定义如下:

=>

然后调用

inc

只会打印当前时间十次(而不是15次)。