简体   繁体   English

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

[英]Why does this Haskell program leak space when compiled with optimizations?

Consider the following toy program that computes all combinations of character substitutions in a word, of the kind often used in passwords. 考虑下面的玩具程序,该程序计算密码中经常使用的单词中字符替换的所有组合。

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"

I compile it with and without optimizations: 我在有和没有优化的情况下编译它:

$ 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 ...

Now test0 and test1 produce the same output, but test1 uses much more memory and spends most of its time in garbage collection: 现在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

Why? 为什么?

I'm using GHC 7.4.1; 我正在使用GHC 7.4.1; I should probably use a newer compiler, but this is what I have handy at the moment, and the fault probably lies with me anyway. 我应该使用一个更新的编译器,但这是我现在所用的方便,而且错误可能在于我。

You want 你要

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

to be compiled into an outer loop and an inner loop. 被编译成外循环和内循环。 But GHC sees that the inner loop does not depend in any way on the outer "loop counter". 但GHC认为内循环并不以任何方式依赖外部“循环计数器”。 Therefore, the full laziness transformation lifts the inner loop out of the outer loop. 因此,完全惰性变换将内环提升出外环。 One fairly effective trick is to hide the fact that the inner loop is independent. 一个相当有效的技巧是隐藏内环是独立的这一事实。 This is done by splitting the inner loop off into a separate function taking a dummy argument, and hiding the dumminess by marking the function as NOINLINE . 这是通过将内部循环拆分为一个带有伪参数的单独函数来完成的,并通过将函数标记为NOINLINE隐藏NOINLINE Then you can call the function with the outer loop counter, and GHC will generally refrain from messing with you. 然后你可以使用外循环计数器调用该函数,GHC通常会避免与你搞乱。

The trick is to cause the recomputation of suffixes, instead of their retention in memory. 诀窍是重新计算后缀,而不是保留在内存中。 It's like with the 就像是

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

definition, where adding the where clause is harmful (or is it powerset (x:xs) = powerset xs ++ map (x:) (powerset xs) ...?). 定义,其中添加where子句是有害的(或者是powerset (x:xs) = powerset xs ++ map (x:) (powerset xs) ......?)。

In your case, the code to try is mapM subst , or 在您的情况下,要尝试的代码是mapM subst ,或

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

You can see that the latter works in the "opposite direction" from your list comprehension code, so maybe just 你可以看到后者在你的列表理解代码的“相反方向”工作,所以也许只是

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

will work, too. 也会奏效。

All these are equivalent, so it's a compiler thing. 所有这些都是等价的,所以它是编译器的东西。 Hopefully someone can provide more specifics about that. 希望有人可以提供更多细节。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM