为什么这个Haskell程序在使用优化编译时会泄漏空间?

时间:2015-06-23 19:46:55

标签: haskell optimization memory-leaks ghc

考虑下面的玩具程序,它计算一个单词中字符替换的所有组合,这种组合通常用在密码中。

import Data.Char (isLower, toUpper)

variants :: String -> [String]
variants "" = [""]
variants (c:s) = [c':s' | c' <- subst c, s' <- variants s]
  where subst 'a' = "aA@"
        subst 'e' = "eE3"
        subst 'i' = "iI1"
        subst 'l' = "lL1"
        subst 'o' = "oO0"
        subst 's' = "sS$5"
        subst 'z' = "zZ2"
        subst x | isLower x = [x, toUpper x]
        subst x = [x]

main :: IO ()
main = putStrLn $ show $ length $ variants "redistributables"

我使用和不使用优化来编译它:

$ ghc -fforce-recomp -Wall Test.hs -o test0
[1 of 1] Compiling Main             ( Test.hs, Test.o )
Linking test0 ...

$ ghc -fforce-recomp -O -Wall Test.hs -o test1
[1 of 1] Compiling Main             ( Test.hs, Test.o )
Linking test1 ...

现在test0test1生成相同的输出,但test1使用更多内存并将大部分时间花在垃圾收集中:

$ ./test0 +RTS -s 2>&1 | grep total
               2 MB total memory in use (0 MB lost due to fragmentation)
  Productivity  93.2% of total user, 93.3% of total elapsed

$ ./test1 +RTS -s 2>&1 | grep total
             188 MB total memory in use (0 MB lost due to fragmentation)
  Productivity  15.0% of total user, 15.0% of total elapsed

为什么?

我正在使用GHC 7.4.1;我应该使用一个更新的编译器,但这是我现在所用的方便,但无论如何,错误可能都在我身上。

2 个答案:

答案 0 :(得分:5)

你想要

variants (c:s) = [c':s' | c' <- subst c, s' <- variants s]

编译成外部循环和内部循环。但GHC认为内循环并不以任何方式依赖外部“循环计数器”。因此,完全惰性变换将内环提升出外环。一个相当有效的技巧是隐藏内环是独立的这一事实。这是通过将内部循环拆分为一个带有伪参数的单独函数,并通过将函数标记为NOINLINE来隐藏虚拟来完成的。然后你可以使用外循环计数器调用该函数,GHC通常会避免弄乱你。

答案 1 :(得分:3)

诀窍是重新计算后缀,而不是保留在内存中。就像

一样
powerset (x:xs) = map (x:) (powerset xs) ++ powerset xs 

定义,添加where子句是有害的(或者是powerset (x:xs) = powerset xs ++ map (x:) (powerset xs) ......)。

在您的情况下,要尝试的代码是mapM subst

variants (c:cs) = variants cs >>= \s-> map (:s) (subst c) 

你可以看到后者在你的列表理解代码的“相反方向”工作,所以也许只是

variants (c:s) = [c':s' | s' <- variants s, c' <- subst c]

也会奏效。

所有这些都是等价的,所以它是编译器的东西。希望有人可以提供更多细节。