列表理解需要太多的记忆

时间:2013-09-12 22:28:09

标签: haskell list-comprehension lazy-evaluation

我是Haskell的初学者并用它来解决Project Euler的大约50个问题,但现在我被困在problem 66。问题是编译后的代码(ghc -O2 --make problem66.hs)在10-20秒后占用了我所有机器的空闲内存。我的代码如下所示:

-- Project Euler, problem 66

diophantine x y d = x^2 - d*y^2 == 1

minimalsolution d = take 1 [(x, y, d) | y <- [2..],
                            let x = round $ sqrt $ fromIntegral (d*y^2+1),
                            diophantine x y d]

issquare x = (round $ sqrt $ fromIntegral x)^2 == x

main = do
    print (map minimalsolution (filter (not . issquare) [1..1000]))

我有一种预感,问题在于minimalsolution列表理解中的无限列表。

我实际上认为由于懒惰,Haskell只会评估列表,直到找到一个元素(因为take 1)并且在路上丢弃diophantine评估为False的所有内容}。我错了吗?

有趣的是,我没有在ghci中看到这种行为。是因为ghci中的处理速度慢得多,我只能等到看到内存消耗爆炸 - 或者是其他东西?

请不要剧透。我想知道的是极端内存消耗来自何处以及我如何解决它。

3 个答案:

答案 0 :(得分:4)

我之前没有说过,所以欢迎扔石头的人。

Haskell确定[2 ..]是一个常量,并且对列表的每个元素重用,尽管只使用该列表中的一个元素;所以它会记住用于计算同一列表的未来元素的列表。你得到d = 61的计算值。


编辑:

有趣的是,这个终止于[1..1000]:

minimalsolution d = take 1 [(x, y, d) | y <- [2..] :: [Int],
                            let x = round $ sqrt $ fromIntegral (d*y^2+1),
                            diophantine x y d]

刚刚添加了:: [Int]。内存使用看起来稳定在1MB。使用Int64可以重现问题。

minimalsolution d = take 1 [(x, y, d) | y <- [2..] :: [Int64],
                            let x = round $ sqrt $ fromIntegral (d*y^2+1),
                            diophantine x y d]

编辑:

嗯,正如所建议的那样,差异是由溢出引起的。 d = 61的解决方案报告为(5983,20568,61),但5983 ^ 2远不及61 * 20568 ^ 2.

答案 1 :(得分:1)

在理解内部为y的每个值创建不必要的Double实例。

我找不到使用没有空间爆炸的列表推导的解决方案。但是使用递归重写会产生稳定的内存配置文件。

diophantine :: Int -> Int -> Int -> Bool
diophantine x y d = x^2 - d*y^2 == 1

minimalsolution ::  Int -> (Int, Int, Int)
minimalsolution d = go 2
  where
    d0 = fromIntegral d
    go a =
      let y = fromIntegral a
          x = round $ sqrt $ (d0*y^2+1) in
      if diophantine x y d then
        (x, y, d)
      else
        go (y+1)

答案 2 :(得分:1)

对于6年后的现在,我已经对其进行了测试,该问题不再出现。使用GHC 8.6.5时,内存消耗保持非常低的水平。我认为这确实是在某个时候已解决的编译器中的问题。