我已经确定了图书馆中似乎包含内存泄漏的一小部分。下面的代码尽可能地小,同时仍然产生与真实代码相同的结果。

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 *进行概要分析并不能揭示太多。

但是,线程范围很有趣:

在我看来,就像我在炸堆一样,因此发生了GC,并且堆大小增加了一倍。的确,当我使用-H4000M运行时,线程范围看起来更均匀(工作量更少,GC却更少),但是我仍然花费整个运行时的60%来进行GC。使用-O2进行编译甚至更糟,因为超过70%的运行时花费在GC上。

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

对于问题2,我意识到堆使用量甚至可能超出我的“预期”内存使用量。但是800MB对我来说似乎过多。 (是我应该查看的号码吗?)

最佳答案

为了解决这样的问题,我通常会在我觉得可能有大量分配的地方,开始用SCC pragma填充代码。在这种情况下,我怀疑e中的c1transformgenUVs中的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,我们可以确定几乎所有这些值都在c1transform中分配。这由-hr确认,它告诉我们谁在这些对象上保留了引用(从而防止了对它们的垃圾收集)。我们可以通过检查c1保留的对象类型来进一步确认-hrc1 -hy是元凶(假设我们已使用{-# SCC c1 #-}对其进行了注释)。
c1保留了如此多的对象这一事实表明,我们不希望对它进行评估。虽然在评估之后c1是一个相当短的向量,但在评估之前,它需要数千个随机种子,关联的闭包以及可能的许多其他对象。
Deepseq ing c1使GC时间从59%降低到23%,并将内存消耗降低了一个数量级。那是return中的终端transform变成了,

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

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

关于haskell - 过多的垃圾回收(和内存使用?),我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/19164151/

10-09 08:36
查看更多