【问题标题】:F# System.OutOfMemoryException with recursive call带有递归调用的 F# System.OutOfMemoryException
【发布时间】:2010-11-08 22:06:35
【问题描述】:

这实际上是 F# 中 Project Euler Problem 14 的解决方案。但是,在尝试计算更大数字的迭代序列时,我遇到了 System.OutOfMemory 异常。如您所见,我正在编写带有尾调用的递归函数。

我遇到了 StackOverFlowException 的问题,因为我在 Visual Studio 中进行调试(禁用尾调用)。我已经记录了in another question。在这里,我在发布模式下运行 - 但是当我将它作为控制台应用程序运行时(在具有 4gb ram 的 windows xp 上),我会出现内存不足的异常。

我真的不知道我是如何将自己编码到这个内存溢出中并希望有人能以我的方式向我展示错误。

let E14_interativeSequence x =

  let rec calc acc startNum =
    match startNum with
    | d when d = 1      -> List.rev (d::acc)
    | e when e%2 = 0    -> calc (e::acc) (e/2)
    | _                 -> calc (startNum::acc) (startNum * 3 + 1)

  let maxNum pl=

    let rec maxPairInternal acc pairList =
        match pairList with
        | []        ->  acc
        | x::xs     ->  if (snd x) > (snd acc) then maxPairInternal x xs
                        else maxPairInternal acc xs

    maxPairInternal (0,0) pl
    |> fst

  // if I lower this to like [2..99999] it will work.
  [2..99999] 
  |> List.map (fun n -> (n,(calc [] n)))
  |> List.map (fun pair -> ((fst pair), (List.length (snd pair))))
  |> maxNum
  |> (fun x-> Console.WriteLine(x))

编辑

根据答案给出的建议,我重写了使用惰性列表并使用 Int64。

#r "FSharp.PowerPack.dll"

let E14_interativeSequence =

  let rec calc acc startNum =
    match startNum with
    | d when d = 1L         -> List.rev (d::acc) |> List.toSeq
    | e when e%2L = 0L      -> calc (e::acc) (e/2L)
    | _                     -> calc (startNum::acc) (startNum * 3L + 1L)

  let maxNum (lazyPairs:LazyList<System.Int64*System.Int64>) =

    let rec maxPairInternal acc (pairs:seq<System.Int64*System.Int64>) =
        match pairs with
        | :? LazyList<System.Int64*System.Int64> as p ->
            match p with
            | LazyList.Cons(x,xs)->  if (snd x) > (snd acc) then maxPairInternal x xs
                                     else maxPairInternal acc xs
            | _                         ->  acc
        | _ -> failwith("not a lazylist of pairs")

    maxPairInternal (0L,0L) lazyPairs
    |> fst

  {2L..999999L}
  |> Seq.map (fun n -> (n,(calc [] n)))
  |> Seq.map (fun pair -> ((fst pair), (Convert.ToInt64(Seq.length (snd pair)))))
  |> LazyList.ofSeq
  |> maxNum

解决问题。不过,我也想看看殷朱的解决方案,哪个更好。

【问题讨论】:

  • 正如@Brian 指出的,Seq 更适合这个问题。事实上,在解决了 Project Euler 中的前 45 个问题后,我发现几乎所有问题都最适合基于 Seq 的解决方案。如果您有兴趣,这里是我对问题 14 的解决方案:projecteulerfun.blogspot.com/2010/05/…(当然,您可能希望等到您的解决方案满意后再进行比较,或者您可能已经满足使用您的算法,但想看看基于 Seq 的实现的外观如何)。
  • 警告,除了 OutOfMemoryException 之外,您的解决方案至少还有一个其他问题,查看我的解决方案可能会被破坏。

标签: recursion f# out-of-memory tail-recursion


【解决方案1】:

正如 Brian 所说,List.* 操作在这里不合适。它们花费了太多的内存。

stackoverflow 问题来自另一个地方。您有两种可能拥有 stackoverflow:calcmaxPairInternal。它必须是第一个,因为第二个与第一个具有相同的深度。然后问题就出现在数字上,3n+1 问题中的数字很容易变得非常大。所以你首先得到一个 int32 溢出,然后你得到一个 stackoverflow。这就是原因。将数字更改为 64 位后,程序运行。

Here is my solution page,在这里你可以看到一个记忆技巧。

open System
let E14_interativeSequence x =

  let rec calc acc startNum =
    match startNum with
    | d when d = 1L      -> List.rev (d::acc)
    | e when e%2L = 0L    -> calc (e::acc) (e/2L)
    | _                 -> calc (startNum::acc) (startNum * 3L + 1L)

  let maxNum pl=

    let rec maxPairInternal acc pairList =
        match pairList with
        | []        ->  acc
        | x::xs     ->  if (snd x) > (snd acc) then maxPairInternal x xs
                        else maxPairInternal acc xs

    maxPairInternal (0L,0) pl
    |> fst

  // if I lower this to like [2..99999] it will work.
  [2L..1000000L] 
  |> Seq.map (fun n -> (n,(calc [] n)))
  |> Seq.maxBy (fun (n, lst) -> List.length lst)
  |> (fun x-> Console.WriteLine(x))

【讨论】:

  • +1 好收获@Yin。我注意到 OP 代码中存在 int32 溢出,但没有连接到内存不足异常;当我在自己的解决方案中遇到同样的缺陷时,它导致了不终止,因为我的策略并不涉及实际构建 collat​​z 链,而只是计算它们的长度。
  • 是的。我不会想出来的。 . .
  • @Kevin Won:如果您怀疑或想要测试代码中发生整数溢出,请将open Microsoft.FSharp.Core.Operators.Checked 添加到您的脚本中。这将整数运算符替换为溢出时抛出的运算符。它会使您的计算(稍微)变慢,所以不要忘记在不再需要它时将其删除。
  • @cfern:很好。我不知道。
【解决方案2】:

如果您将 List.map 更改为 Seq.map(并重新处理 maxPairInternal 以迭代 seq),这可能会有所帮助。现在,在处理整个结构以获得单个数字结果之前,您正在将所有数据一次显示在一个巨大的结构中。最好通过 Seq 懒惰地执行此操作,只需创建一行,然后将其与下一行进行比较,一次创建一行然后丢弃它。

我现在没有时间编写我的建议,但如果您仍有问题,请告诉我,我会重新考虑。

【讨论】:

    【解决方案3】:

    停止尝试在任何地方使用列表,这不是 Haskell!不要到处写fst pairsnd pair,这不是Lisp!

    如果你想要一个简单的 F# 解决方案,你可以直接这样做,而无需创建任何中间数据结构:

    let rec f = function
      | 1L -> 0
      | n when n % 2L = 0L -> 1 + f(n / 2L)
      | n -> 1 + f(3L * n + 1L)
    
    let rec g (li, i) = function
      | 1L -> i
      | n -> g (max (li, i) (f n, n)) (n - 1L)
    
    let euler14 n = g (0, 1L) n
    

    在我的上网本上大约需要 15 秒。如果您想要更节省时间的东西,请通过数组重用以前的结果:

    let rec inside (a : _ array) n =
      if n <= 1L || a.[int n] > 0s then a.[int n] else
        let p =
          if n &&& 1L = 0L then inside a (n >>> 1) else
            let n = 3L*n + 1L
            if n < int64 a.Length then inside a n else outside a n
        a.[int n] <- 1s + p
        1s + p
    and outside (a : _ array) n =
      let n = if n &&& 1L = 0L then n >>> 1 else 3L*n + 1L
      1s + if n < int64 a.Length then inside a n else outside a n
    
    let euler14 n =
      let a = Array.create (n+1) 0s
      let a = Array.Parallel.init (n+1) (fun n -> inside a (int64 n))
      let i = Array.findIndex (Array.reduce max a |> (=)) a
      i, a.[i]
    

    在我的上网本上大约需要 0.2 秒。

    【讨论】:

    • @jon:在样式首选项之外,您反对列表和配对函数的原因是什么?我试图了解您的观点以及为什么您认为我的解决方案是滥用。虽然 F# 肯定不是 Haskell 或 Lisp,但它肯定有一个借鉴了父母双方的血统。
    • 当元素很少(最好是零)时,列表可能是一个很好的集合,特别是在逻辑编程的上下文中,但这里不是这种情况,因此它们不适合这种情况。 fstsnd 函数几乎总是最好避免使用模式匹配。
    • 而不是写d when d=1(在Lisp中看起来像COND)你应该简单地写模式1。不要给startNum 别名de,这会造成混淆,始终使用相同的名称。将x::xs -&gt; if (snd x) &gt; .. 替换为(_, v as x)::xs -&gt; if v &gt; ..。将fun pair -&gt; ((fst pair), (List.length (snd pair))) 替换为fun (k, v) -&gt; k, List.length v,删除多余的括号以及不必要且低效的fstsnd,留下更短、更快的代码。
    • 永远不要建立大的列表,尤其是像[2..99999]这样的连续整数。砍伐你的管道。不要急于建立任意长的列表来计算它们的长度。永远不要引入不必要的运行时类型测试。惰性列表在实践中几乎毫无用处,因此您需要一个令人信服的理由才能将它们加入其中,在这种情况下,它们只会掩盖您的方法的潜在问题。
    • 我认为它看起来像 Haskellish,现在我来搜索 Haskell 解决方案,您的代码看起来像是 Haskell wiki 中第一个解决方案的字面翻译。当您尝试翻译 Haskell 代码时,您需要非常小心,因为 Haskell 程序往往缺乏使程序在现实世界中可计算的本质,即可预测的时间和空间。
    【解决方案4】:

    在寻找 Microsoft.FSharp.Core.Operators.Checked 时找到了这个。 我刚学 F#,所以我想我会参加 Project Euler 14 挑战赛。

    这使用递归而不是尾递归。 对我来说大约需要 3.1 秒,但优点是我几乎可以理解它。

    let Collatz (n:int64) = if n % 2L = 0L then n / 2L else n * 3L + 1L
    
    let rec CollatzLength (current:int64) (acc:int) =
        match current with 
        | 1L -> acc
        | _ -> CollatzLength (Collatz current) (acc + 1)
    
    let collatzSeq (max:int64) = 
        seq{
            for i in 1L..max do
                yield i, CollatzLength i 0
        }
    
    let collatz = Seq.toList(collatzSeq 1000000L)
    
    let result, steps = List.maxBy snd collatz
    

    【讨论】:

      猜你喜欢
      • 2011-09-13
      • 1970-01-01
      • 2013-10-08
      • 1970-01-01
      • 2012-12-28
      • 1970-01-01
      • 2021-02-03
      • 2020-10-07
      • 1970-01-01
      相关资源
      最近更新 更多