【问题标题】:Haskell - Recursion Stack OverflowHaskell - 递归堆栈溢出
【发布时间】:2019-07-08 04:26:03
【问题描述】:

我试图将所有 n 从 1 加到一个非常大的数字(现在为 10**9),但它会导致堆栈溢出。此外,我不认为在 1 处停止并在不同行中求和 n 是最有效的方法,但下面的代码是我对 Haskell 的全部知识。我真的不太了解函数式编程,我想尽可能多地解释一下。 (我也试过把 $! strict 放在最后一行,这在其他地方被告知,但它没有改变。如果你解释我可以做这个递归函数的最有效的方法,我会很高兴。)

main :: IO()

summ 1 = 1
summ n = 1/(n**2) + summ (n-1)

expected_value = pi*pi/6
errorPercent n = n / expected_value * 100

main = do
    putStr "%"
    print (errorPercent (summ $! (10**9)))

【问题讨论】:

  • 哦对了,你可以把summ写成summ n = sum [ 1 / i^2 | i <- [1..n] ]。不错,对吧? (确保在编译时对此进行了优化)
  • @luqui - 我想你希望sum [ 1 / i^2 | i <- reverse [1..n] ] 避免精度损失。
  • @Omnifarious 有趣!我从来没有想过这样的问题。我不太明白为什么会这样......
  • @luqui - 由于四舍五入而发生。当加在一起的数字大小相似时,浮点数效果最好。否则,从小数中添加的位会从大数的末尾旋转,甚至不会改变它。所以,即使你添加了几十万个这样的小数字,它们根本不会改变原来的数字。
  • @luqui - 根据经验,是的。我还可以想象,在某些特定情况下,不同的排序可能会更好。其他操作也有其他规则。我不知道它们都是什么,但我知道充分利用浮点精度需要仔细考虑。

标签: haskell recursion stack-overflow


【解决方案1】:

这里的问题是,在整个 10^9 次递归调用结束之前,无法开始计算总和。本质上,你是在计算

1/(n**2) + ( 1/((n-1)**2) + ( 1/((n-2)**2) + ....

括号阻止开始求和。相反,我们希望拥有

(( 1/(n**2) + 1/((n-1)**2) ) + 1/((n-2)**2) ) + ....

最简单的方法是使用“累加器”附加参数:

summ 1 acc = 1 + acc
summ n acc = summ (n-1) $! acc + 1/(n**2)

main = do
    putStr "%"
    print (errorPercent (summ (10^9) 0))  -- set acc to 0 at the beginning

为了提高性能,我建议向summ 添加类型签名,例如summ :: Int -> Double -> Double.


下面的完整程序。这在 12 秒内运行 (ghc -O2)。

summ :: Int -> Double -> Double
summ 1 acc = 1 + acc
summ n acc = summ (n-1) $! acc + 1 / (fromIntegral n**2)

main :: IO ()
main = do
    putStr "%"
    print (summ (10^9) 0)  -- set acc to 0 at the beginning

【讨论】:

  • 它成功了,我可以从 10^9 得到输出,但它真的很慢。我目前正在等待 10^10 和 10^9 花了大约 20 分钟。这个速度是正常的还是有办法让它更快?
  • @Terobero 您是否在编译后运行代码并进行优化,例如-O2?如果您使用 GHCi 运行它,通常会很慢。
  • @Terobero 我不知道 VSCode。我猜它确实使用 GHC 编译代码,因为它运行 main 而不是给你一个 REPL 提示(对吗?)。它的选项中应该有一些地方可以添加-O2(如果尚未启用)。从命令行我会使用stack ghc -- File.hs -O2
  • @Terobero 查看最后的编辑。没有errorPercent,开启-O2,这里10^9的程序在12s内运行。你的 20 分钟太多了。
  • @Terobero 对于整数变量,使用Int 或其他整数类型更快更安全。使用 Double 可能会导致一些微妙的问题,例如x == x+1x 足够大时Double,舍入错误很容易破坏程序。我倾向于避免使用浮点,除非一个不精确的近似值足够好(例如,当我们需要精确比较时,例如if x == 1 then ...)。 luqui 在他的回答中说明了为什么在这里使用 Double 不起作用。
【解决方案2】:

chi 回答了一个问题,我认为这是主要问题,但还有其他问题困扰着我。当您说10**9 时,您会得到一个浮点 数(因为** 是“小数”幂)。然后您使用浮点相等来检查递归的基本情况。

summ 1 = ...

这样做的问题是,随着参数变大,很可能由于数值错误,您几乎不会错过基本情况并永远下降为负值。

summ 4 =        ... summ 3
summ 3 =        ... summ 2.000001
summ 2.000001 = ... summ 1.000001 
summ 1.000001 = ... summ 0.000001  -- BASE CASE MISSED!
summ 0.000001 = ... summ (-1.000001)
summ (-1.000001) = ... summ (-2.000001)

等等。如果您没有从 109 次调用中获得堆栈溢出,那么您肯定会无限次调用。

你应该在整数上定义你的函数,这样就不会有舍入误差

summ :: Int -> Double
summ 1 = 1
summ n = 1 / (fromIntegral n ** 2) + summ (n - 1)
--            ^^^^^^^^^^^^
-- conversion necessary to go from Int to Double

main = ... print (summ (10 ^ 9))
--                      ^^^^^^
--      use integral exponentiation (^) instead of (**)

或使用更宽容的基本情况

summ :: Double -> Double
summ n | n <= 1 = 1
summ n = 1 / (n ** 2) + summ (n - 1)

在任何一种情况下,你都绝对应该接受 chi 的建议,以累加器的方式来做这件事,而且你也绝对应该放一个类型签名。

如果你好奇,这里是more on how you get stack overflows in Haskell

【讨论】:

    猜你喜欢
    • 2015-08-05
    • 2012-07-08
    • 2015-04-04
    • 2017-01-20
    • 2018-12-02
    • 2017-09-06
    • 2017-09-29
    • 1970-01-01
    • 2011-08-22
    相关资源
    最近更新 更多