6

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

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对我来说似乎太大了。(这甚至是我应该看的数字吗?)

4

1 回答 1

6

为了解决这样的问题,我通常会SCC在我认为可能有大量分配的地方用编译指示乱扔代码开始。在这种情况下,我怀疑eand c1intransformgenUVsin sample

...

transform :: (Monad m, RandomGen g)
           => StateT g m [U.Vector Int64]
transform = do
      e <- {-# SCC e #-} liftM (U.map round . uncurry (U.++) . U.unzip) $ U.replicateM (vecLen `div` 2) sample
      c1 <- {-# SCC 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 = {-# 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是一个相当短的向量,但在评估之前它需要数千个随机种子、相关的闭包以及可能的许多其他对象。

Deepseqingc1将 GC 时间从 59% 降低到 23%,并将内存消耗减少了一个数量级。那就是终端轮流到returntransform

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

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

于 2013-10-04T02:06:30.827 回答