【问题标题】:Reducing a large stream without stack overflowing在没有堆栈溢出的情况下减少大流
【发布时间】:2012-05-01 02:42:16
【问题描述】:

1我正在尝试制作一个无限制的阶乘函数(只是出于好奇。)这适用于大 n(尝试高达 100000 并且它似乎有效,尽管我无法检查输出值正确性,因为它很大!)

(BigInt(1) to n).reduceRight(_*_)

但我担心整个BigInt(1) to n 范围可能都在内存中,而我只需要reduceRight 的元素一个元素。看看 Scala 的标准库代码,它看起来像 BigInt(1) to n 实际上输出整个 Range 而不是懒惰的 Stream 但我发现 Stream.range 我可以像这样使用它(注意 n+1,流范围是独家)

Stream.range[BigInt](BigInt(1), BigInt(n+1)).reduceRight(_*_)

它适用于n=10000(由于某种原因,它需要更长的时间!这让我觉得也许正常范围实际上也是Stream?)但是对于n=100000,我得到这个堆栈溢出

java.lang.StackOverflowError
    at java.math.BigInteger.add(Unknown Source)
    at scala.math.BigInt.$plus(BigInt.scala:161)
    at scala.math.Numeric$BigIntIsIntegral$class.plus(Numeric.scala:28)
    at scala.math.Numeric$BigIntIsIntegral$.plus(Numeric.scala:40)
    at scala.math.Numeric$BigIntIsIntegral$.plus(Numeric.scala:40)
    at scala.math.Numeric$Ops.$plus(Numeric.scala:208)
    at scala.collection.immutable.Stream$$anonfun$range$1.apply(Stream.scala:695)
    at scala.collection.immutable.Stream$$anonfun$range$1.apply(Stream.scala:695)
    at scala.collection.immutable.Stream$Cons.tail(Stream.scala:634)
    at scala.collection.immutable.Stream$Cons.tail(Stream.scala:626)
    at scala.collection.LinearSeqOptimized$class.reduceRight(LinearSeqOptimized.scala:130)
    at scala.collection.immutable.Stream.reduceRight(Stream.scala:47)
    at scala.collection.LinearSeqOptimized$class.reduceRight(LinearSeqOptimized.scala:131)
    at scala.collection.immutable.Stream.reduceRight(Stream.scala:47)
    at scala.collection.LinearSeqOptimized$class.reduceRight(LinearSeqOptimized.scala:131)
    at scala.collection.immutable.Stream.reduceRight(Stream.scala:47)
    ...

很明显reduceRight是这样称呼自己的

reduceRight(reduceRight(first, second, op), third, op)...

因此堆栈溢出。我认为它不是尾调用优化的,因为它首先减少然后在返回值之前运行,所以它不能被优化。那我怎么能解决这个问题呢?我应该放弃函数式方法并以自定义命令式代码为目标吗?

让我感到非常奇怪的是(BigInt(1) to n).reduceRight(_*_) 不会溢出大的n,而使用流几乎相同...这里发生了什么?

【问题讨论】:

    标签: scala stream range stack-overflow reduce


    【解决方案1】:

    您的第一个解决方案will create a list in memory 存储相反的序列是正确的。您可以简单地使用reduceLeft(在范围上没有这个问题),但这将通过相反方向的范围。如果由于某种原因你想从大端开始但保持reduceLeft的懒惰,你可以创建一个向后的Range

    def fact(n: Int) = (BigInt(n) to 1 by -1).reduceLeft(_ * _)
    

    可能有other ways你可以轻松优化这个功能。

    【讨论】:

    • 优化的好主意。谢谢!
    • 你的顺序倒过来了。有序比倒序更快,foldLeft 会按照您要求的顺序执行。
    • @Rex:实际上,如果BigInt乘法的成本与两个数字的位数的乘积成正比,那么这两个方向都没有优势,对吧?无论如何,我已经编辑了答案以使其成为非问题。
    • @TravisBrown 在我非常有限的测试中,您提出的方向比反向(增加)方向慢约 30%。
    • @Travis - 你第一次关于表演是对的;您对评估顺序的看法是错误的。例如:使用您的指标,2*2*2*1000 的正向成本为 1+1+4,反向成本为 4+4+4。
    【解决方案2】:

    reduceLeft 被实现为在流上进行计算(并按顺序调用)。可以如下验证:

    Stream.range(1,10).map(i => print(i)).reduceLeft((a,b) => println("r"))
    

    或者你可以使用尾递归:

    final def fac(n: BigInt, prod: BigInt = BigInt(1)): BigInt = {
      if (n<2) prod else fac(n-1,prod*n)
    }
    

    (正如 Travis 指出的那样,先将小数相乘会更快,这会占用额外的一行)。

    【讨论】:

    • 在另一个答案中查看我的评论(也适用于此处。)另外:我想避免使用 TCR 的常见阶乘实现,因为这是使用惰性范围的练习。
    • @kaoD - 它不需要范围并且不从最后一个元素开始。查看示例(粘贴到 REPL)。
    【解决方案3】:

    尝试改用reduceLeftreduceRight 从流的末尾开始,因此会强制评估每个元素——这正是您想要避免的。

    【讨论】:

    • 也想过,但是,它不需要整个Range 开始吗? IIRC,reduceLeft 从最后一个元素开始,但必须计算整个 Range 才能使最后一个元素存在(这实际上是我不想要的。)
    • reduceLeft 从左边开始,所以在第一个元素,比如foldLeft
    • 是的,我只是在看TraversableOnce 并看到了它。我错误地将Right 作为评估的方向,而不是它的开始。谢谢!
    • 顺便说一句,这对我来说更快:Stream.range(BigInt(1), BigInt(n)).par.reduce(_*_).
    【解决方案4】:
    def fac (n: BigInt=1, m:BigInt=1): Stream[BigInt] = 
      Stream.cons (n, fac (n * m, m+1))
    
    fac().take (10).toList
    res27: List[BigInt] = List(1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880)
    

    也适用于 take(10000).toList

    【讨论】:

    猜你喜欢
    • 2012-04-23
    • 2017-01-23
    • 2011-12-30
    • 2013-02-25
    • 2014-12-25
    • 2014-07-29
    • 2014-03-14
    • 2016-08-30
    • 1970-01-01
    相关资源
    最近更新 更多