考虑下面的玩具程序,它计算一个单词中字符替换的所有组合,这种组合通常用在密码中。
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 ...
现在test0
和test1
生成相同的输出,但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;我应该使用一个更新的编译器,但这是我现在所用的方便,但无论如何,错误可能都在我身上。
答案 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]
也会奏效。
所有这些都是等价的,所以它是编译器的东西。希望有人可以提供更多细节。