过多的垃圾收集(和内存使用?)

时间:2013-10-03 16:17:08

标签: haskell memory-leaks profiling heap monads

我发现了一个似乎包含内存泄漏的库的一小部分。下面的代码尽可能小,但仍然产生与实际代码相同的结果。

import System.Random
import Control.Monad.State
import Control.Monad.Loops
import Control.DeepSeq
import Data.Int (Int64)
import qualified Data.Vector.Unboxed as U

vecLen = 2048

main = flip evalStateT (mkStdGen 13) $ do
    let k = 64
    cs <- replicateM k transform
    let sizeCs = k*2*7*vecLen*8 -- 64 samples, 2 elts per list, each of len 7*vecLen, 8 bytes per Int64
    (force cs) `seq` lift $ putStr $ "Expected to use ~ " ++ (show ((fromIntegral sizeCs) / 1000000 :: Double)) ++ " MB of memory\n"

transform :: (Monad m, RandomGen g)
           => StateT g m [U.Vector Int64]
transform = do
      e <- liftM ((U.map round) . (uncurry (U.++)) . U.unzip) $ U.replicateM (vecLen `div` 2) sample
      c1 <- U.replicateM (7*vecLen) $ state random
      return [U.concat $ replicate 7 e, c1]

sample :: (RandomGen g, Monad m) => StateT g m (Double, Double)
sample = do 
    let genUVs = liftM2 (,) (state $ randomR (-1,1)) (state $ randomR (-1,1))
        -- memory usage drops and productivity increases to about 58% if I set the guard to "False" (the real code needs a guard here)
        uvGuard (u,v) = u+v >= 2 -- False -- 
    (u,v) <- iterateWhile uvGuard genUVs
    return (u, v)

删除任何更多代码可显着提高内存使用/ GC,时间或两者的性能。但是,我需要计算上面的代码,所以真正的代码不能更简单。 例如,如果我使e和c1都从sample获取值,则代码使用27 MB内存并在GC中花费9%的运行时间。如果我使e和c1都使用state random,我使用大约400MB的内存,并且只在GC中花费32%的运行时间。

主要参数是vecLen,我真正需要大约8192.为了加快分析,我使用vecLen=2048生成了以下所有结果,但问题更严重,因为vecLen增加了

使用

进行编译
ghc test -rtsopts

我明白了:

> ./test +RTS -sstderr
Working...
Expected to use ~ 14.680064 MB of memory
Done
   3,961,219,208 bytes allocated in the heap
   2,409,953,720 bytes copied during GC
     383,698,504 bytes maximum residency (17 sample(s))
       3,214,456 bytes maximum slop
             869 MB total memory in use (0 MB lost due to fragmentation)

                                    Tot time (elapsed)  Avg pause  Max pause
  Gen  0      7002 colls,     0 par    1.33s    1.32s     0.0002s    0.0034s
  Gen  1        17 colls,     0 par    1.60s    1.84s     0.1080s    0.5426s

  INIT    time    0.00s  (  0.00s elapsed)
  MUT     time    2.08s  (  2.12s elapsed)
  GC      time    2.93s  (  3.16s elapsed)
  EXIT    time    0.00s  (  0.03s elapsed)
  Total   time    5.01s  (  5.30s elapsed)

  %GC     time      58.5%  (59.5% elapsed)

  Alloc rate    1,904,312,376 bytes per MUT second

  Productivity  41.5% of total user, 39.2% of total elapsed


real    0m5.306s
user    0m5.008s
sys 0m0.252s

使用-p或-h *进行性能分析并不会显示太多,至少对我而言。

然而,线程范围很有意思:threadscope

在我看来,我正在吹堆,所以GC正在发生,堆大小翻倍。实际上,当我使用-H4000M运行时,线程范围看起来稍微更均匀(工作量更少,双重GC),但我仍然花费大约60%的整个运行时间来执行GC。使用-O2进行编译会更糟糕,超过70%的运行时间用于GC。

问题: 1.为什么GC运行这么多? 2. 我的堆使用量是否意外大?如果是这样,为什么?

对于问题2,我意识到堆使用量可能超过我的“预期”内存使用量,即使是很多。但800MB似乎对我来说太过分了。 (这是我应该看的数字吗?)

1 个答案:

答案 0 :(得分:5)

为了攻击这样的问题,我经常会先用SCC编译语乱丢代码,无论我觉得哪里可能有大量的分配。在这种情况下,我怀疑e中的c1transform以及genUVs中的sample

...

transform :: (Monad m, RandomGen g)
           => StateT g m [U.Vector Int64]
transform = do
      e <- {-# SCC hi1 #-} liftM (U.map round . uncurry (U.++) . U.unzip) $ U.replicateM (vecLen `div` 2) sample
      c1 <- {-# SCC hi2 #-} U.replicateM (7*vecLen) $ state random
      return [U.concat $ replicate 7 e, c1]

sample :: (RandomGen g, Monad m) => StateT g m (Double, Double)
sample = do 
    let genUVs = {-# SCC genUVs #-} liftM2 (,) (state $ randomR (-1,1)) (state $ randomR (-1,1))
        -- memory usage drops and productivity increases to about 58% if I set the guard to "False" (the real code needs a guard here)
        uvGuard (u,v) = u+v >= 2 -- False -- 
    (u,v) <- iterateWhile uvGuard genUVs
    return $ (u, v)

我们首先查看-hy以查看相关对象的类型。这揭示了许多不同类型,包括IntegerInt32StdGenInt(,)。使用-hc,我们可以确定几乎所有这些值都是在c1 transform中分配的。这由-hr确认,它告诉我们谁拥有对这些对象的引用(从而防止它们被垃圾收集)。我们可以通过检查c1保留的对象类型来进一步确认-hrc1 -hy是罪魁祸首(假设我们已使用{-# SCC c1 #-}注释它。)

c1保留这么多对象这一事实表明它在我们希望它时没有被评估。在评估之后c1是一个相当短的向量,在评估之前它需要几千个随机种子,相关的闭包,以及可能的其他一些对象。

Deepseq c1将GC时间从59%提高到23%,并将内存消耗降低一个数量级。这是return中的终端transform转入,

deepseq c1 $ return [U.concat $ replicate 7 e, c1]

在此之后,配置文件看起来相当合理,最大空间用户在ARR_WORDS(按预期)分配的transform大约为10MB,后跟一些元组,可能来自genUVs