【问题标题】:Excessive garbage collection (and memory use?)过多的垃圾收集(和内存使用?)
【发布时间】:2013-10-10 10:39:38
【问题描述】:

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

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

【问题讨论】:

  • 您是否尝试过使用vector.Unboxed?看来您只使用带有 Int64 的向量。它应该可以帮助您减少内存使用量。
  • 是的,原代码使用了Unboxed向量,内存使用也差不多。为方便起见,我删除了拆箱实例。
  • @MdxBhmt 可以肯定的是,我只是将示例代码更改为使用 Unboxed 向量(包括 Zq 的 50 行 Unbox 实例)并获得相同的内存使用和时间。
  • 我只能说每次使用replicate n $ state $ random 都会发生泄漏。无法解释原因。
  • @DonStewart -O2 在修复泄漏之前将我的工作效率从 40% 降低到 30%。

标签: haskell memory-leaks profiling heap-memory monads


【解决方案1】:

要解决这样的问题,我通常会在我觉得可能有大量分配的地方用SCC pragma 乱扔代码开始。在这种情况下,我怀疑transform 中的ec1sample 中的genUVs

...

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,我们可以确定几乎所有这些值都分配在c1 中的transform 中。 -hr 证实了这一点,它告诉我们谁持有这些对象的引用(从而防止它们被垃圾收集)。我们可以通过检查-hrc1 -hy 保留的对象类型来进一步确认c1 是罪魁祸首(假设我们已经用{-# SCC c1 #-} 对其进行了注释)。

c1 保留了这么多对象的事实表明,当我们希望它被评估时,它并没有被评估。虽然在评估之后c1 是一个相当短的向量,但在评估之前它需要数千个随机种子、相关的闭包,并且可能还有许多其他对象。

Deepseqing c1 将 GC 时间从 59% 提高到 23%,并将内存消耗降低一个数量级。也就是transform中的终端return变成,

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

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

【讨论】:

  • “被 c1 保留”是什么意思?这究竟是“c1 是一个未经评估的重击”,还是可以是其他东西?为什么不评估 e/c1?我应该预料到吗?我将此修复程序放入我的真实代码中,其好处是巨大的。谢谢!
  • 是的,由于 Haskell 的懒惰,c1 永远不会被强制进入 transform。在大多数情况下,除非需要,否则不会计算表达式。这意味着 c1 仍然是未评估的 thunk,直到您将其强制到 main 中,这仅在所有 k transforms 完成后才会发生。在此之前,评估c1 所需的所有值都必须保留在内存中,从而导致您的内存使用率很高。
  • 请注意,我的原始答案中有一个错误,泄漏的值不是Vectors,而是几种不同的类型,包括StdGen。这强烈表明它们由于 thunk 泄漏而被保留。
  • 看看带注释的代码会很有用,你能把它包括在答案中吗?
  • 幸运的是,我自己尝试在真实代码中进行一些分析会发现 GHC 中的一个错误。叹息。
猜你喜欢
  • 2017-03-15
  • 2017-10-30
  • 2021-04-30
  • 2012-11-30
  • 2012-06-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多